From 3970141614de42a3b250b1413315364617bc224a Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Mon, 30 Nov 2020 08:09:47 +0100 Subject: [PATCH 01/17] First commit Signed-off-by: Koen Schockaert Signed-off-by: QbusKoen --- bundles/org.openhab.binding.qbus/NOTICE | 13 + bundles/org.openhab.binding.qbus/README.md | 103 ++ bundles/org.openhab.binding.qbus/doc/Logo.JPG | Bin 0 -> 12981 bytes bundles/org.openhab.binding.qbus/pom.xml | 17 + .../src/main/feature/feature.xml | 23 + .../qbus/internal/QbusBindingConstants.java | 98 ++ .../qbus/internal/QbusBridgeHandler.java | 241 +++++ .../qbus/internal/QbusHandlerFactory.java | 69 ++ .../handler/QbusBistabielHandler.java | 183 ++++ .../qbus/internal/handler/QbusCO2Handler.java | 167 ++++ .../internal/handler/QbusDimmerHandler.java | 250 +++++ .../qbus/internal/handler/QbusRolHandler.java | 264 +++++ .../internal/handler/QbusSceneHandler.java | 181 ++++ .../handler/QbusThermostatHandler.java | 183 ++++ .../qbus/internal/protocol/QMessageCmd.java | 75 ++ .../internal/protocol/QMessageListMap.java | 38 + .../qbus/internal/protocol/QMessageMap.java | 36 + .../qbus/internal/protocol/QThermostat.java | 175 ++++ .../qbus/internal/protocol/QbusBistabiel.java | 103 ++ .../qbus/internal/protocol/QbusCO2.java | 75 ++ .../internal/protocol/QbusCommunication.java | 936 ++++++++++++++++++ .../qbus/internal/protocol/QbusDimmer.java | 102 ++ .../internal/protocol/QbusMessageBase.java | 54 + .../protocol/QbusMessageDeserializer.java | 110 ++ .../qbus/internal/protocol/QbusRol.java | 141 +++ .../qbus/internal/protocol/QbusScene.java | 103 ++ .../main/resources/OH-INF/binding/binding.xml | 10 + .../OH-INF/i18n/qbus_xx_XX.properties | 17 + .../resources/OH-INF/thing/thing-types.xml | 247 +++++ 29 files changed, 4014 insertions(+) create mode 100644 bundles/org.openhab.binding.qbus/NOTICE create mode 100644 bundles/org.openhab.binding.qbus/README.md create mode 100644 bundles/org.openhab.binding.qbus/doc/Logo.JPG create mode 100644 bundles/org.openhab.binding.qbus/pom.xml create mode 100644 bundles/org.openhab.binding.qbus/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageListMap.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageMap.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QThermostat.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties create mode 100644 bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml diff --git a/bundles/org.openhab.binding.qbus/NOTICE b/bundles/org.openhab.binding.qbus/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/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.qbus/README.md b/bundles/org.openhab.binding.qbus/README.md new file mode 100644 index 0000000000000..89b5efe07d84d --- /dev/null +++ b/bundles/org.openhab.binding.qbus/README.md @@ -0,0 +1,103 @@ +# Qbus Binding +![Qbus Logo](doc/Logo.JPG) + +This binding for Qbus communicates for all controllers of the Qbus home automation system. + +More information about Qbus can be found here: +[Qbus](https://qbus.be) + +This is the link to the [Qbus forum](https://qbusforum.be). This forum is mainly in dutch and you can find a lot of information about the pre testings af this binding and offers a way to communicate with other users. + +We also host a site which contains a [manual](https://manualoh.schockaert.tk/) where you can find lot's of information. + +The controllers can not communicate directly with openHAB, therefore we developed a Client/Server application which you must install prior to enable this binding. +More information can be found here: +[Qbus Client/Server](https://github.com/QbusKoen/QbusClientServer) + +With this binding you can control and read almost every output from the Qbus system. + +## Supported Things + +The following things are supported by the Qbus Binding: +- Dimmer 1 button, 2 button and clc as _dimmer_ +- Bistabiel, Timer1-3, Interval as _onOff_ +- Thermostats - normal and PID as _thermosats_ +- Scenes as _scene_ +- CO2 as _co2_ +- Rollershutter and rollerhutter with slats as _rollershutter_ + +For now the folowing Qbus things are not yet supported but will come: +- DMX +- Timer 4 & 5 +- HVAC +- Humidity +- Renson +- Duco +- Kinetura +- Energy monitor +- Weather station + + +## Discovery + +The discovery service is not yet implemented but the System Manager III software of Qbus generates things and item files from the programming, which you can use directly in openHAB. + +## Thing Configuration + +This is an example of the things configuration file: + +``` +Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, refresh=10 ] { + dimmer 1 "ToonzaalLED" [ dimmerId=100 ] + onOff 30 "Toonzaal230V" [ bistabielId=76 ] + thermostat 50 "Service" [ thermostatId=99 ] + scene 70 "Disco" [ sceneId=36 ] + co2 100 "Productie" [ co2Id=26 ] + rollershutter 120 "Roller1" [ rolId=268 ] + rollershutter_slats 121 "Roller2" [ rolId=264 ] +} +``` +The Bridge connects to the QbusServer, so if the Client/Server application is installed on the same machine then localhost can be used as address. sn is the serial nr of your controller, port should allways be 8447, except in special installations as it is the communication port of the Server application. refresh is a time in minutes which will check the status of the server and reconnects if connection is broken after this time. + +## Channels + +| channel | type | description | +|---------------------|---------------|---------------------------------------------------------| +| onOff | switch | This is the channel for Bistable, Timers and Intervals | +| dimmer | brightness | This is the channel for Dimmers 1&2 buttons and CLC | +| scene | Switch | This is the channel for scenes | +| co2 | co2 | This is the channel for CO2 sensors | +| rollershutter | rollershutter | This is the channel for rollershutters | +| rollershutter_slats | rollershutter | This is the channel for rollershutters with slats | +| thermostat | setpoint | This is the channel for thermostats setpoint | +| thermostat | measured | This is the channel for thermostats currenttemp | +| thermostat | mode | This is the channel for thermostats mode | + + +## Full Example + +### Things: +``` +Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, refresh=10 ] { + dimmer 1 "ToonzaalLED" [ dimmerId=100 ] + onOff 30 "Toonzaal230V" [ bistabielId=76 ] + thermostat 50 "Service" [ thermostatId=99 ] + scene 70 "Disco" [ sceneId=36 ] + co2 100 "Productie" [ co2Id=26 ] + rollershutter 120 "Roller1" [ rolId=268 ] + rollershutter_slats 121 "Roller2" [ rolId=264 ] +} +``` +### Items: +``` +Dimmer ToonzaalLED [ "Lighting" ] {channel="qbus:dimmer:CTD007841:1:brightness"} +Switch Toonzaal230V {channel="qbus:onOff:CTD007841:30:switch"} +Number:Temperature ServiceSP"[%.1f °C]" (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:setpoint"} +Number:Temperature ServiceCT"[%.1f °C]" (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:measured"} +Number ServiceMode (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:mode",ihc="0x33c311" , autoupdate="true"} +Switch Disco {channel="qbus:scene:CTD007841:36:scene"} +Number ProductieCO2 {channel="qbus:co2:CTD007841:100:co2"} +Rollershutter Roller1 {channel="qbus:rollershutter:CTD007841:120:rollershutter"} +Rollershutter Roller2 {channel="qbus:rollershutter_slats:CTD007841:121:rollershutter"} +Dimmer Roller2_slats {channel="qbus:rollershutter_slats:CTD007841:121:slats"} +``` \ No newline at end of file diff --git a/bundles/org.openhab.binding.qbus/doc/Logo.JPG b/bundles/org.openhab.binding.qbus/doc/Logo.JPG new file mode 100644 index 0000000000000000000000000000000000000000..4db5d75ae4403d9d1aa945287c333e9dc85d6130 GIT binary patch literal 12981 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.qbus/pom.xml b/bundles/org.openhab.binding.qbus/pom.xml new file mode 100644 index 0000000000000..3cda2c4358f52 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.0.0-SNAPSHOT + + + org.openhab.binding.qbus + + openHAB Add-ons :: Bundles :: Qbus Binding + + diff --git a/bundles/org.openhab.binding.qbus/src/main/feature/feature.xml b/bundles/org.openhab.binding.qbus/src/main/feature/feature.xml new file mode 100644 index 0000000000000..fcae4ee3aee20 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/feature/feature.xml @@ -0,0 +1,23 @@ + + + + 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.qbus/${project.version} + + diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java new file mode 100644 index 0000000000000..5cd8e6bc2b10f --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java @@ -0,0 +1,98 @@ +/** + * 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.qbus.internal; + +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.openhab.core.thing.ThingTypeUID; + +/** + * The {@link QbusBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Koen Schockaert - Initial contribution + */ +@NonNullByDefault +public class QbusBindingConstants { + + private static final String BINDING_ID = "qbus"; + + // bridge + public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "bridge"); + public static final Set BRIDGE_THING_TYPES_UIDS = Collections.singleton(BRIDGE_THING_TYPE); + // Bridge config properties + public static final String CONFIG_HOST_NAME = "addr"; + public static final String CONFIG_PORT = "port"; + public static final String CONFIG_REFRESH = "refresh"; + public static final String CONFIG_SN = "sn"; + + // generic thing types + public static final ThingTypeUID THING_TYPE_CO2 = new ThingTypeUID(BINDING_ID, "co2"); + public static final ThingTypeUID THING_TYPE_SCENE = new ThingTypeUID(BINDING_ID, "scene"); + public static final ThingTypeUID THING_TYPE_TIMER_LIGHT = new ThingTypeUID(BINDING_ID, "onOff"); + public static final ThingTypeUID THING_TYPE_ON_OFF_LIGHT = new ThingTypeUID(BINDING_ID, "onOff"); + public static final ThingTypeUID THING_TYPE_DIMMABLE_LIGHT = new ThingTypeUID(BINDING_ID, "dimmer"); + public static final ThingTypeUID THING_TYPE_ROLLERSHUTTER = new ThingTypeUID(BINDING_ID, "rollershutter"); + public static final ThingTypeUID THING_TYPE_ROLLERSHUTTER_SLATS = new ThingTypeUID(BINDING_ID, + "rollershutter_slats"); + public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat"); + // List of all Thing Type UIDs + public static final Set SCENE_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_SCENE).collect(Collectors.toSet())); + + public static final Set CO2_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_CO2).collect(Collectors.toSet())); + + public static final Set ROLLERSHUTTER_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_ROLLERSHUTTER).collect(Collectors.toSet())); + + public static final Set ROLLERSHUTTER_SLATS_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_ROLLERSHUTTER_SLATS).collect(Collectors.toSet())); + + public static final Set BISTABIEL_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_ON_OFF_LIGHT, THING_TYPE_TIMER_LIGHT).collect(Collectors.toSet())); + + public static final Set THERMOSTAT_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_THERMOSTAT).collect(Collectors.toSet())); + + public static final Set DIMMER_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_ON_OFF_LIGHT, THING_TYPE_DIMMABLE_LIGHT).collect(Collectors.toSet())); + + public static final Set SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(Stream + .of(THING_TYPE_TIMER_LIGHT, THING_TYPE_ON_OFF_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_THERMOSTAT, + THING_TYPE_SCENE, THING_TYPE_CO2, THING_TYPE_ROLLERSHUTTER, THING_TYPE_ROLLERSHUTTER_SLATS) + .collect(Collectors.toSet())); + + // List of all Channel ids + public static final String CHANNEL_SWITCH = "switch"; + public static final String CHANNEL_BRIGHTNESS = "brightness"; + public static final String CHANNEL_MEASURED = "measured"; + public static final String CHANNEL_SETPOINT = "setpoint"; + public static final String CHANNEL_MODE = "mode"; + public static final String CHANNEL_CO2 = "co2"; + public static final String CHANNEL_ROLLERSHUTTER = "rollershutter"; + public static final String CHANNEL_SLATS = "slats"; + + // Thing config properties + public static final String CONFIG_BISTABIEL_ID = "bistabielId"; + public static final String CONFIG_DIMMER_ID = "dimmerId"; + public static final String CONFIG_THERMOSTAT_ID = "thermostatId"; + public static final String CONFIG_SCENE_ID = "sceneId"; + public static final String CONFIG_CO2_ID = "co2Id"; + public static final String CONFIG_ROLLERSHUTTER_ID = "rolId"; + public static final String CONFIG_STEP_VALUE = "step"; +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java new file mode 100644 index 0000000000000..4377a1fd5803f --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java @@ -0,0 +1,241 @@ +/** + * 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.qbus.internal; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link QbusBridgeHandler} is the handler for a Qbus controller + * + * @author Koen Schockaert - Initial Contribution + */ +public class QbusBridgeHandler extends BaseBridgeHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusBridgeHandler.class); + + private QbusCommunication qbusComm; + + private ScheduledFuture refreshTimer; + + public QbusBridgeHandler(Bridge Bridge) { + super(Bridge); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // There is nothing to handle in the bridge handler + } + + @Override + public void initialize() { + logger.debug("QBUS: initializing bridge handler"); + + Configuration config = this.getConfig(); + InetAddress addr = getAddr(); + int port = getPort(); + + logger.debug("Qbus: bridge handler host {}, port {}", addr, port); + + if (addr != null) { + createCommunicationObject(addr, port); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Qbus: cannot resolve bridge IP with hostname " + config.get(CONFIG_HOST_NAME)); + } + } + + /** + * Create communication object to Qbus server and start communication. + * + * @param addr : IP address of Qbus server + * @param port : Communication port of QbusServer + * @param sn : Serial number of Controller + */ + private void createCommunicationObject(InetAddress addr, int port) { + Configuration config = this.getConfig(); + scheduler.submit(() -> { + qbusComm = new QbusCommunication(); + + // Set callback from Qbus object to this bridge to be able to take bridge + // offline when non-resolvable communication error occurs. + setBridgeCallBack(); + + qbusComm.startCommunication(); + if (!qbusComm.communicationActive()) { + qbusComm = null; + bridgeOffline(); + return; + } + + updateStatus(ThingStatus.ONLINE); + + Integer refreshInterval = ((Number) config.get(CONFIG_REFRESH)).intValue(); + setupRefreshTimer(refreshInterval); + + }); + } + + private void setBridgeCallBack() { + this.qbusComm.setBridgeCallBack(this); + } + + /** + * Schedule future communication refresh. + * + * @param interval_config Time before refresh in minutes. + */ + private void setupRefreshTimer(Integer refreshInterval) { + if (this.refreshTimer != null) { + this.refreshTimer.cancel(true); + this.refreshTimer = null; + } + + if ((refreshInterval == null) || (refreshInterval == 0)) { + return; + } + + // This timer will restart the bridge connection periodically + logger.debug("Qbus: Checking for Client communication every {} min", refreshInterval); + this.refreshTimer = scheduler.scheduleWithFixedDelay(() -> { + logger.debug("Qbus: check communication after timerinterval"); + + if (!qbusComm.communicationActive()) { + logger.debug("Qbus: Restarting communication"); + qbusComm.restartCommunication(); + + if (!qbusComm.communicationActive()) { + qbusComm = null; + bridgeOffline(); + updateStatus(ThingStatus.OFFLINE); + return; + } + + // updateStatus(ThingStatus.ONLINE); + updateStatus(ThingStatus.ONLINE); + } else { + logger.debug("Qbus: Communication still active"); + } + + }, refreshInterval, refreshInterval, TimeUnit.MINUTES); + } + + /** + * Take bridge offline when error in communication with Qbus server. This method can also be + * called directly from {@link QbusCommunication} object. + */ + public void bridgeOffline() { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Qbus: error starting bridge connection"); + } + + /** + * Put bridge online when error in communication resolved. + */ + public void bridgeOnline() { + updateStatus(ThingStatus.ONLINE); + } + + @Override + public boolean isInitialized() { + return true; + } + + @Override + public void dispose() { + if (this.refreshTimer != null) { + this.refreshTimer.cancel(true); + } + this.refreshTimer = null; + } + + @Override + public void handleConfigurationUpdate(Map configurationParameters) { + Configuration configuration = editConfiguration(); + for (Entry configurationParmeter : configurationParameters.entrySet()) { + configuration.put(configurationParmeter.getKey(), configurationParmeter.getValue()); + } + updateConfiguration(configuration); + + scheduler.submit(() -> { + + updateStatus(ThingStatus.ONLINE); + + Integer refreshInterval = ((Number) configuration.get(CONFIG_REFRESH)).intValue(); + setupRefreshTimer(refreshInterval); + }); + } + + /** + * Get the Qbus communication object. + * + * @return Qbus communication object + */ + public QbusCommunication getCommunication() { + return this.qbusComm; + } + + /** + * Get the IP-address of the Qbus server. + * + * @return the addr + */ + public InetAddress getAddr() { + Configuration config = this.getConfig(); + InetAddress addr = null; + try { + addr = InetAddress.getByName((String) config.get(CONFIG_HOST_NAME)); + } catch (UnknownHostException e) { + logger.debug("Qbus: Cannot resolve hostname {} to IP adress", config.get(CONFIG_HOST_NAME)); + } + return addr; + } + + /** + * Get the listening port of the Qbus server. + * + * @return the port + */ + public int getPort() { + Configuration config = this.getConfig(); + return ((Number) config.get(CONFIG_PORT)).intValue(); + } + + /** + * Get the serial nr of the Qbus server. + * + * @return the sn + */ + public String getSn() { + Configuration config = this.getConfig(); + return ((String) config.get(CONFIG_SN)); + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java new file mode 100644 index 0000000000000..5a1cb996c4a91 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java @@ -0,0 +1,69 @@ +/** + * 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.qbus.internal; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; + +import org.openhab.binding.qbus.internal.handler.QbusBistabielHandler; +import org.openhab.binding.qbus.internal.handler.QbusCO2Handler; +import org.openhab.binding.qbus.internal.handler.QbusDimmerHandler; +import org.openhab.binding.qbus.internal.handler.QbusRolHandler; +import org.openhab.binding.qbus.internal.handler.QbusSceneHandler; +import org.openhab.binding.qbus.internal.handler.QbusThermostatHandler; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link qbusHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Koen Schockaert - Initial Contribution + */ + +@Component(service = ThingHandlerFactory.class, configurationPid = "binding.qbus") +public class QbusHandlerFactory extends BaseThingHandlerFactory { + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID) || BRIDGE_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected ThingHandler createHandler(Thing thing) { + if (BRIDGE_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + QbusBridgeHandler handler = new QbusBridgeHandler((Bridge) thing); + return handler; + } else if (SCENE_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusSceneHandler(thing); + } else if (BISTABIEL_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusBistabielHandler(thing); + } else if (THERMOSTAT_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusThermostatHandler(thing); + } else if (DIMMER_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusDimmerHandler(thing); + } else if (CO2_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusCO2Handler(thing); + } else if (ROLLERSHUTTER_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusRolHandler(thing); + } else if (ROLLERSHUTTER_SLATS_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusRolHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java new file mode 100644 index 0000000000000..fb45b86151077 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java @@ -0,0 +1,183 @@ +/** + * 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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusBistabiel; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusBistabielHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public class QbusBistabielHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusBistabielHandler.class); + + // private volatile int prevBistabielState; + + public QbusBistabielHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + Integer BistabielId = ((Number) this.getConfig().get(CONFIG_BISTABIEL_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute bistabiel " + BistabielId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute bistabiel " + BistabielId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: bridge communication not initialized when trying to execute bistabiel " + BistabielId); + return; + } + + QbusBistabiel QBistabiel = QComm.getBistabiel().get(BistabielId); + + if (QComm.communicationActive()) { + handleCommandSelection(QBistabiel, channelUID, command); + } else { + // We lost connection but the connection object is there, so was correctly started. + // Try to restart communication. + // This can be expensive, therefore do it in a job. + scheduler.submit(() -> { + QComm.restartCommunication(); + // If still not active, take thing offline and return. + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + // Also put the bridge back online + QBridgeHandler.bridgeOnline(); + + // And finally handle the command + handleCommandSelection(QBistabiel, channelUID, command); + }); + } + } + + private void handleCommandSelection(QbusBistabiel QBistabiel, ChannelUID channelUID, Command command) { + logger.debug("Qbus: handle command {} for {}", command, channelUID); + + if (command == REFRESH) { + handleStateUpdate(QBistabiel); + return; + } + + handleSwitchCommand(QBistabiel, command); + updateStatus(ThingStatus.ONLINE); + } + + private void handleSwitchCommand(QbusBistabiel QBistabiel, Command command) { + if (command instanceof OnOffType) { + OnOffType s = (OnOffType) command; + + @SuppressWarnings("null") + String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + + if (s == OnOffType.OFF) { + QBistabiel.execute(0, sn); + } else { + QBistabiel.execute(100, sn); + } + } + } + + @Override + public void initialize() { + Configuration config = this.getConfig(); + + Integer BistabielId = ((Number) config.get(CONFIG_BISTABIEL_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for bistabiel " + BistabielId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for bistabiel " + BistabielId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + if (QComm == null || !QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: no connection with Qbus server, could not initialize bistabiel " + BistabielId); + return; + } + + QbusBistabiel QBistabiel = QComm.getBistabiel().get(BistabielId); + + // int bistabielState = QBistabiel.getState(); + + // this.prevBistabielState = bistabielState; + QBistabiel.setThingHandler(this); + + Map properties = new HashMap<>(); + + thing.setProperties(properties); + + handleStateUpdate(QBistabiel); + + logger.debug("Qbus: bistabiel intialized {}", BistabielId); + } + + /** + * Method to update state of channel, called from Qbus Bistabiel. + */ + public void handleStateUpdate(QbusBistabiel QBistabiel) { + + int bistabielState = QBistabiel.getState(); + + updateState(CHANNEL_SWITCH, (bistabielState == 0) ? OnOffType.OFF : OnOffType.ON); + updateStatus(ThingStatus.ONLINE); + + // this.prevBistabielState = bistabielState; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java new file mode 100644 index 0000000000000..b500901e62073 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java @@ -0,0 +1,167 @@ +/** + * 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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusCO2; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusCO2Handler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public class QbusCO2Handler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusCO2Handler.class); + + // private volatile int prevCO2State; + + public QbusCO2Handler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + Integer CO2Id = ((Number) this.getConfig().get(CONFIG_CO2_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute CO2 " + CO2Id); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute CO2 " + CO2Id); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: bridge communication not initialized when trying to execute CO2 " + CO2Id); + return; + } + + QbusCO2 QCO2 = QComm.getCo2().get(CO2Id); + + if (QComm.communicationActive()) { + handleCommandSelection(QCO2, channelUID, command); + } else { + // We lost connection but the connection object is there, so was correctly started. + // Try to restart communication. + // This can be expensive, therefore do it in a job. + scheduler.submit(() -> { + QComm.restartCommunication(); + // If still not active, take thing offline and return. + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + // Also put the bridge back online + QBridgeHandler.bridgeOnline(); + + // And finally handle the command + handleCommandSelection(QCO2, channelUID, command); + }); + } + } + + private void handleCommandSelection(QbusCO2 QCO2, ChannelUID channelUID, Command command) { + logger.debug("Qbus: handle command {} for {}", command, channelUID); + + if (command == REFRESH) { + handleStateUpdate(QCO2); + return; + } + + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void initialize() { + Configuration config = this.getConfig(); + + Integer CO2Id = ((Number) config.get(CONFIG_CO2_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for CO2 " + CO2Id); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for CO2 " + CO2Id); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + if (QComm == null || !QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: no connection with Qbus server, could not initialize CO2 " + CO2Id); + return; + } + + QbusCO2 QCO2 = QComm.getCo2().get(CO2Id); + + // int actionState = QCO2.getState(); + + // this.prevCO2State = actionState; + QCO2.setThingHandler(this); + + Map properties = new HashMap<>(); + + thing.setProperties(properties); + + handleStateUpdate(QCO2); + + logger.debug("Qbus: CO2 intialized {}", CO2Id); + } + + /** + * Method to update state of channel, called from Qbus CO2. + */ + public void handleStateUpdate(QbusCO2 QCO2) { + + int CO2State = QCO2.getState(); + + updateState(CHANNEL_CO2, new DecimalType(CO2State)); + updateStatus(ThingStatus.ONLINE); + + // this.prevCO2State = CO2State; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java new file mode 100644 index 0000000000000..0ff3d2412fa00 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java @@ -0,0 +1,250 @@ +/** + * 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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.binding.qbus.internal.protocol.QbusDimmer; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusDimmerHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public class QbusDimmerHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusDimmerHandler.class); + + // private volatile int prevDimmerState; + + public QbusDimmerHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + Integer dimmerId = ((Number) this.getConfig().get(CONFIG_DIMMER_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute dimmer " + dimmerId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute dimmer " + dimmerId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: bridge communication not initialized when trying to execute dimmer " + dimmerId); + return; + } + + QbusDimmer QDimmer = QComm.getDimmer().get(dimmerId); + + /* + * if (QDimmer == null) { + * updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + * "Qbus: dimmerId " + dimmerId + " does not match a dimmer in the controller"); + * return; + * } + */ + if (QComm.communicationActive()) { + handleCommandSelection(QDimmer, channelUID, command); + } else { + // We lost connection but the connection object is there, so was correctly started. + // Try to restart communication. + // This can be expensive, therefore do it in a job. + scheduler.submit(() -> { + QComm.restartCommunication(); + // If still not active, take thing offline and return. + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + // Also put the bridge back online + QBridgeHandler.bridgeOnline(); + + // And finally handle the command + handleCommandSelection(QDimmer, channelUID, command); + }); + } + } + + private void handleCommandSelection(QbusDimmer QDimmer, ChannelUID channelUID, Command command) { + logger.debug("Qbus: handle command {} for {}", command, channelUID); + + if (command == REFRESH) { + handleStateUpdate(QDimmer); + return; + } + + switch (channelUID.getId()) { + case CHANNEL_SWITCH: + handleSwitchCommand(QDimmer, command); + updateStatus(ThingStatus.ONLINE); + break; + + case CHANNEL_BRIGHTNESS: + handleBrightnessCommand(QDimmer, command); + updateStatus(ThingStatus.ONLINE); + break; + + default: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: channel unknown " + channelUID.getId()); + } + } + + private void handleSwitchCommand(QbusDimmer QDimmer, Command command) { + + @SuppressWarnings("null") + String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + + if (command instanceof OnOffType) { + OnOffType s = (OnOffType) command; + if (s == OnOffType.OFF) { + QDimmer.execute(0, sn); + } else { + QDimmer.execute(100, sn); + } + } + } + + private void handleBrightnessCommand(QbusDimmer QDimmer, Command command) { + // Bridge QBridge = getBridge(); + @SuppressWarnings("null") + String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + + if (command instanceof OnOffType) { + OnOffType s = (OnOffType) command; + if (s == OnOffType.OFF) { + QDimmer.execute(0, sn); + } else { + QDimmer.execute(100, sn); + } + } else if (command instanceof IncreaseDecreaseType) { + IncreaseDecreaseType s = (IncreaseDecreaseType) command; + int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); + int currentValue = QDimmer.getState(); + int newValue; + if (s == IncreaseDecreaseType.INCREASE) { + newValue = currentValue + stepValue; + // round down to step multiple + newValue = newValue - newValue % stepValue; + QDimmer.execute(newValue > 100 ? 100 : newValue, sn); + } else { + newValue = currentValue - stepValue; + // round up to step multiple + newValue = newValue + newValue % stepValue; + QDimmer.execute(newValue < 0 ? 0 : newValue, sn); + } + } else if (command instanceof PercentType) { + PercentType p = (PercentType) command; + if (p == PercentType.ZERO) { + QDimmer.execute(0, sn); + } else { + QDimmer.execute(p.intValue(), sn); + } + } + } + + @Override + public void initialize() { + Configuration config = this.getConfig(); + + Integer dimmerId = ((Number) config.get(CONFIG_DIMMER_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for dimmer " + dimmerId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for dimmer " + dimmerId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + if (QComm == null || !QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: no connection with Qbus, could not initialize dimmer " + dimmerId); + return; + } + + QbusDimmer QDimmer = QComm.getDimmer().get(dimmerId); + /* + * if (QDimmer == null) { + * updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + * "Qbus: dimmerId does not match an action in the server " + dimmerId); + * return; + * } + */ + // int actionState = QDimmer.getState(); + + // this.prevDimmerState = actionState; + QDimmer.setThingHandler(this); + + Map properties = new HashMap<>(); + + thing.setProperties(properties); + + handleStateUpdate(QDimmer); + + logger.debug("Qbus: dimmer intialized {}", dimmerId); + } + + /** + * Method to update state of channel, called from Qbus Dimmer. + */ + public void handleStateUpdate(QbusDimmer QDimmer) { + + int dimmerState = QDimmer.getState(); + + updateState(CHANNEL_BRIGHTNESS, new PercentType(dimmerState)); + + updateStatus(ThingStatus.ONLINE); + + // this.prevDimmerState = dimmerState; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java new file mode 100644 index 0000000000000..fa9bad35e3458 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.binding.qbus.internal.protocol.QbusRol; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusRolHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public class QbusRolHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusRolHandler.class); + + // private volatile int prevRolState; + // private volatile int prevSlatState; + + public QbusRolHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + Integer RolId = ((Number) this.getConfig().get(CONFIG_ROLLERSHUTTER_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute Slats " + RolId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute Slats " + RolId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: bridge communication not initialized when trying to execute Slats " + RolId); + return; + } + + QbusRol QRol = QComm.getRol().get(RolId); + + if (QComm.communicationActive()) { + handleCommandSelection(QRol, channelUID, command); + } else { + // We lost connection but the connection object is there, so was correctly started. + // Try to restart communication. + // This can be expensive, therefore do it in a job. + scheduler.submit(() -> { + QComm.restartCommunication(); + // If still not active, take thing offline and return. + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + // Also put the bridge back online + QBridgeHandler.bridgeOnline(); + + // And finally handle the command + handleCommandSelection(QRol, channelUID, command); + }); + } + } + + private void handleCommandSelection(QbusRol qRol, ChannelUID channelUID, Command command) { + logger.debug("Qbus: handle command {} for {}", command, channelUID); + + if (command == REFRESH) { + handleStateUpdate(qRol); + return; + } + + switch (channelUID.getId()) { + case CHANNEL_ROLLERSHUTTER: + handleBrightnessCommand(qRol, command); + updateStatus(ThingStatus.ONLINE); + break; + + case CHANNEL_SLATS: + handleSlatsCommand(qRol, command); + updateStatus(ThingStatus.ONLINE); + break; + + default: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: channel unknown " + channelUID.getId()); + } + } + + private void handleBrightnessCommand(QbusRol QRol, Command command) { + @SuppressWarnings("null") + String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + + if (command instanceof org.openhab.core.library.types.UpDownType) { + org.openhab.core.library.types.UpDownType s = (org.openhab.core.library.types.UpDownType) command; + if (s == org.openhab.core.library.types.UpDownType.DOWN) { + QRol.execute(0, sn); + } else { + QRol.execute(100, sn); + } + } else if (command instanceof IncreaseDecreaseType) { + IncreaseDecreaseType s = (IncreaseDecreaseType) command; + int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); + int currentValue = QRol.getState(); + int newValue; + if (s == IncreaseDecreaseType.INCREASE) { + newValue = currentValue + stepValue; + // round down to step multiple + newValue = newValue - newValue % stepValue; + QRol.execute(newValue > 100 ? 100 : newValue, sn); + } else { + newValue = currentValue - stepValue; + // round up to step multiple + newValue = newValue + newValue % stepValue; + QRol.execute(newValue < 0 ? 0 : newValue, sn); + } + } else if (command instanceof PercentType) { + PercentType p = (PercentType) command; + if (p == PercentType.ZERO) { + QRol.execute(0, sn); + } else { + QRol.execute(p.intValue(), sn); + } + } + } + + private void handleSlatsCommand(QbusRol QRol, Command command) { + @SuppressWarnings("null") + String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + + if (command instanceof org.openhab.core.library.types.UpDownType) { + org.openhab.core.library.types.UpDownType s = (org.openhab.core.library.types.UpDownType) command; + if (s == org.openhab.core.library.types.UpDownType.DOWN) { + QRol.executeSlats(0, sn); + } else { + QRol.executeSlats(100, sn); + } + } else if (command instanceof IncreaseDecreaseType) { + IncreaseDecreaseType s = (IncreaseDecreaseType) command; + int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); + int currentValue = QRol.getState(); + int newValue; + if (s == IncreaseDecreaseType.INCREASE) { + newValue = currentValue + stepValue; + // round down to step multiple + newValue = newValue - newValue % stepValue; + QRol.executeSlats(newValue > 100 ? 100 : newValue, sn); + } else { + newValue = currentValue - stepValue; + // round up to step multiple + newValue = newValue + newValue % stepValue; + QRol.executeSlats(newValue < 0 ? 0 : newValue, sn); + } + } else if (command instanceof PercentType) { + PercentType p = (PercentType) command; + if (p == PercentType.ZERO) { + QRol.executeSlats(0, sn); + } else { + QRol.executeSlats(p.intValue(), sn); + } + } + } + + @Override + public void initialize() { + Configuration config = this.getConfig(); + + Integer RolId = ((Number) config.get(CONFIG_ROLLERSHUTTER_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for Slats " + RolId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for Slats " + RolId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + if (QComm == null || !QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: no connection with Qbus server, could not initialize Slats " + RolId); + return; + } + + QbusRol QRol = QComm.getRol().get(RolId); + /* + * int rolState = QRol.getState(); + * int slatState = QRol.getStateSlats(); + * + * this.prevRolState = rolState; + * this.prevSlatState = slatState; + * + */ + QRol.setThingHandler(this); + + Map properties = new HashMap<>(); + + thing.setProperties(properties); + + handleStateUpdate(QRol); + + logger.debug("Qbus: Slats intialized {}", RolId); + } + + /** + * Method to update state of channel, called from Qbus Slats. + */ + public void handleStateUpdate(QbusRol qRol) { + + int rolState = qRol.getState().intValue(); + int slatState = qRol.getStateSlats().intValue(); + + updateState(CHANNEL_ROLLERSHUTTER, new PercentType(rolState)); + updateState(CHANNEL_SLATS, new PercentType(slatState)); + updateStatus(ThingStatus.ONLINE); + + // this.prevRolState = rolState; + // this.prevSlatState = slatState; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java new file mode 100644 index 0000000000000..ca810f3bd4fe7 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java @@ -0,0 +1,181 @@ +/** + * 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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.binding.qbus.internal.protocol.QbusScene; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusSceneHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public class QbusSceneHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusSceneHandler.class); + + // private volatile int prevSceneState; + + public QbusSceneHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + Integer SceneId = ((Number) this.getConfig().get(CONFIG_SCENE_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute Scene " + SceneId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute Scene " + SceneId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: bridge communication not initialized when trying to execute scene " + SceneId); + return; + } + + QbusScene QScene = QComm.getScenes().get(SceneId); + + if (QComm.communicationActive()) { + handleCommandSelection(QScene, channelUID, command); + } else { + // We lost connection but the connection object is there, so was correctly started. + // Try to restart communication. + // This can be expensive, therefore do it in a job. + scheduler.submit(() -> { + QComm.restartCommunication(); + // If still not active, take thing offline and return. + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + // Also put the bridge back online + QBridgeHandler.bridgeOnline(); + + // And finally handle the command + handleCommandSelection(QScene, channelUID, command); + }); + } + } + + private void handleCommandSelection(QbusScene QScene, ChannelUID channelUID, Command command) { + logger.debug("Qbus: handle command {} for {}", command, channelUID); + + if (command == REFRESH) { + handleStateUpdate(QScene); + return; + } + + handleSwitchCommand(QScene, command); + updateStatus(ThingStatus.ONLINE); + } + + private void handleSwitchCommand(QbusScene QScene, Command command) { + @SuppressWarnings("null") + String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + if (command instanceof OnOffType) { + OnOffType s = (OnOffType) command; + if (s == OnOffType.OFF) { + QScene.execute(0, sn); + } else { + QScene.execute(100, sn); + } + } + } + + @Override + public void initialize() { + Configuration config = this.getConfig(); + + Integer SceneId = ((Number) config.get(CONFIG_SCENE_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for scene " + SceneId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for scene " + SceneId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + if (QComm == null || !QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: no connection with Qbus server, could not initialize scene " + SceneId); + return; + } + + QbusScene QScene = QComm.getScenes().get(SceneId); + + // int sceneState = QScene.getState(); + /* + * this.prevSceneState = sceneState; + * QScene.setThingHandler(this); + */ + Map properties = new HashMap<>(); + + thing.setProperties(properties); + + handleStateUpdate(QScene); + + logger.debug("Qbus: scene intialized {}", SceneId); + } + + /** + * Method to update state of channel, called from Qbus Scene. + */ + public void handleStateUpdate(QbusScene QScene) { + + int sceneState = QScene.getState(); + + updateState(CHANNEL_SWITCH, (sceneState == 0) ? OnOffType.OFF : OnOffType.ON); + updateStatus(ThingStatus.ONLINE); + + // this.prevSceneState = sceneState; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java new file mode 100644 index 0000000000000..3120718e73e01 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java @@ -0,0 +1,183 @@ +/** + * 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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.library.unit.SIUnits.CELSIUS; +import static org.openhab.core.types.RefreshType.REFRESH; + +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QThermostat; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusThermostatHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public class QbusThermostatHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusThermostatHandler.class); + + public QbusThermostatHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + Integer thermostatId = ((Number) this.getConfig().get(CONFIG_THERMOSTAT_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute thermostat command " + thermostatId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute thermostat command " + thermostatId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: bridge communication not initialized when trying to execute thermostat command " + + thermostatId); + return; + } + + QThermostat qThermostat = QComm.getThermostats().get(thermostatId); + + if (QComm.communicationActive()) { + handleCommandSelection(qThermostat, channelUID, command); + } else { + scheduler.submit(() -> { + QComm.restartCommunication(); + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + QBridgeHandler.bridgeOnline(); + handleCommandSelection(qThermostat, channelUID, command); + }); + } + } + + @SuppressWarnings("unchecked") + private void handleCommandSelection(QThermostat qThermostat, ChannelUID channelUID, Command command) { + logger.debug("Qbus: handle command {} for {}", command, channelUID); + @SuppressWarnings("null") + String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + + if (REFRESH.equals(command)) { + handleStateUpdate(qThermostat); + return; + } + + switch (channelUID.getId()) { + case CHANNEL_MEASURED: + updateStatus(ThingStatus.ONLINE); + break; + + case CHANNEL_MODE: + if (command instanceof DecimalType) { + qThermostat.executeMode(((DecimalType) command).intValue(), sn); + } + updateStatus(ThingStatus.ONLINE); + break; + + case CHANNEL_SETPOINT: + if (command instanceof QuantityType) { + qThermostat.executeSetpoint(((QuantityType) command).doubleValue(), sn); + } + updateStatus(ThingStatus.ONLINE); + + break; + + default: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: channel unknown " + channelUID.getId()); + } + } + + @Override + public void initialize() { + Configuration config = this.getConfig(); + + Integer thermostatId = ((Number) config.get(CONFIG_THERMOSTAT_ID)).intValue(); + + Bridge nhcBridge = getBridge(); + if (nhcBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for thermostat " + thermostatId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) nhcBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for thermostat " + thermostatId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + if (QComm == null || !QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: no connection with Qbus server, could not initialize thermostat " + thermostatId); + return; + } + + QThermostat qThermostat = QComm.getThermostats().get(thermostatId); + + qThermostat.setThingHandler(this); + + handleStateUpdate(qThermostat); + + logger.debug("Qbus: thermostat intialized {}", thermostatId); + } + + /** + * Method to update state of all channels, called from Qbus thermostat. + * + * @param qThermostat Qbus thermostat + * + */ + public void handleStateUpdate(QThermostat qThermostat) { + + updateState(CHANNEL_MEASURED, new QuantityType(qThermostat.getMeasured(), CELSIUS)); + + updateState(CHANNEL_SETPOINT, new QuantityType(qThermostat.getSetpoint(), CELSIUS)); + + updateState(CHANNEL_MODE, new DecimalType(qThermostat.getMode())); + + updateStatus(ThingStatus.ONLINE); + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java new file mode 100644 index 0000000000000..78b89d7543ef5 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java @@ -0,0 +1,75 @@ +/** + * 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.qbus.internal.protocol; + +/** + * Class {@link QbusMessageCmd} used as input to gson to send commands to Qbus. Extends + * {@link QbusMessageBase}. + *

+ * Example: {"cmd":"executebistabiel","id":1,"value1":0} + * + * @author Koen Schockaert - Initial Contribution + */ + +@SuppressWarnings("unused") +class QMessageCmd extends QbusMessageBase { + + private int id; + private Integer pos; + private Integer value1; + private Integer value2; + private Integer value3; + private Integer mode; + private Double setpoint; + private String ctdsn; + + QMessageCmd(String cmd) { + super.setCmd(cmd); + } + + QMessageCmd(String cmd, int id) { + this(cmd); + this.id = id; + } + + QMessageCmd(String cmd, int id, Integer value1) { + this(cmd, id); + this.value1 = value1; + } + + QMessageCmd(String cmd, int id, Integer value1, Integer value2) { + this(cmd, id, value1); + this.value2 = value2; + } + + QMessageCmd(String cmd, int id, Integer value1, Integer value2, Integer value3) { + this(cmd, id, value1); + this.value2 = value2; + this.value3 = value3; + } + + QMessageCmd withMode(Integer mode) { + this.mode = mode; + return this; + } + + QMessageCmd withSetpoint(Double d) { + this.setpoint = d; + return this; + } + + QMessageCmd withSn(String sn) { + this.ctdsn = sn; + return this; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageListMap.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageListMap.java new file mode 100644 index 0000000000000..1bc73610537a0 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageListMap.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.qbus.internal.protocol; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Class {@link QbusMessageListMap} used as output from gson for cmd or event feedback from Qbus where the + * data part is enclosed by [] and contains a list of json strings. Extends {@link QbusMessageBase}. + *

+ * + * @author Koen Schockaert - Initial Contribution + */ + +class QMessageListMap extends QbusMessageBase { + + private List> data = new ArrayList<>(); + + List> getData() { + return this.data; + } + + void setData(List> data) { + this.data = data; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageMap.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageMap.java new file mode 100644 index 0000000000000..139ecee65bd47 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageMap.java @@ -0,0 +1,36 @@ +/** + * 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.qbus.internal.protocol; + +import java.util.HashMap; +import java.util.Map; + +/** + * Class {@link QbusMessageMap} used as output from gson for cmd or event feedback from Qbus where the + * data part is a simple json string. Extends {@link QbusMessageBase}. + *

+ * + * @author Koen Schockaert - Initial Contribution + */ +class QMessageMap extends QbusMessageBase { + + private Map data = new HashMap<>(); + + Map getData() { + return this.data; + } + + void setData(Map data) { + this.data = data; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QThermostat.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QThermostat.java new file mode 100644 index 0000000000000..4663194c57fa8 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QThermostat.java @@ -0,0 +1,175 @@ +/** + * 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.qbus.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusThermostatHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QThermostat} class represents the thermostat Qbus communication object. It contains all + * fields representing a Qbus thermostat and has methods to set the thermostat mode and setpoint in Qbus and + * receive thermostat updates. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public final class QThermostat { + + private final Logger logger = LoggerFactory.getLogger(QThermostat.class); + + @Nullable + private QbusCommunication qComm; + + private int id; + private Double measured = 0.0; + private Double setpoint = 0.0; + private Integer mode = 0; + + @Nullable + private QbusThermostatHandler thingHandler; + + QThermostat(int id) { + this.id = id; + } + + /** + * Update all values of the thermostat + * + * @param measured current temperature in 1°C multiples + * @param setpoint the setpoint temperature in 1°C multiples + * @param mode 0="Manueel", 1="Vorst", 2="Economisch", 3="Comfort", 4="Nacht" + */ + public void updateState(Double measured, Double setpoint, Integer mode) { + setMeasured(measured); + setSetpoint(setpoint); + setMode(mode); + + QbusThermostatHandler handler = thingHandler; + if (handler != null) { + logger.debug("Qbus: update channels for {}", id); + handler.handleStateUpdate(this); + } + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to the termostat is initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the thermostat receives an update from the Qbus IP-interface. + * + * @param handler + */ + public void setThingHandler(QbusThermostatHandler handler) { + this.thingHandler = handler; + } + + /** + * This method sets a pointer to the qComm object of class {@link QbusCommuncation}. + * This is then used to be able to call back the sendCommand method in this class to send a command to the + * Qbus IP-interface when.. + * + * @param qComm + */ + public void setQComm(QbusCommunication qComm) { + this.qComm = qComm; + } + + /** + * Get measured temperature. + * + * @return measured temperature in 0.5°C multiples + */ + public double getMeasured() { + return this.measured; + } + + /** + * Set measured temperature. + * + * @param measured + */ + private void setMeasured(Double measured) { + this.measured = measured; + } + + /** + * Get setpoint + * + * @return the setpoint temperature in 1°C multiples + */ + public double getSetpoint() { + return this.setpoint; + } + + /** + * Set setpoint temperature. + * + * @param setpoint + */ + private void setSetpoint(Double setpoint) { + this.setpoint = setpoint; + } + + /** + * Get the thermostat mode. + * + * @return the mode: 0="Manueel", 1="Vorst", 2="Economisch", 3="Comfort", 4="Nacht" + */ + public Integer getMode() { + return mode; + } + + /** + * Set the thermostat + * + * mode: 0="Manueel", 1="Vorst", 2="Economisch", 3="Comfort", 4="Nacht" + */ + private void setMode(Integer mode) { + this.mode = mode; + } + + /** + * Sends thermostat mode to Qbus. + * + * @param mode + * @param sn + */ + public void executeMode(int mode, String sn) { + logger.debug("Qbus: execute thermostat mode {} for {} on {}", mode, this.id, sn); + + QMessageCmd qCmd = new QMessageCmd("executethermostat", this.id).withMode(mode).withSn(sn); + + QbusCommunication comm = qComm; + if (comm != null) { + comm.sendMessage(qCmd); + } + } + + /** + * Sends setpoint to Qbus. + * + * @param d + */ + public void executeSetpoint(double d, String sn) { + logger.debug("Qbus: execute thermostat setpoint {} for {} on {}", d, this.id, sn); + + QMessageCmd qCmd = new QMessageCmd("executethermostat", this.id).withSetpoint(d).withSn(sn); + + QbusCommunication comm = qComm; + if (comm != null) { + comm.sendMessage(qCmd); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java new file mode 100644 index 0000000000000..52547826ee8f6 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java @@ -0,0 +1,103 @@ +/** + * 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.qbus.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusBistabielHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusBistabiel} class represents the Qbus BISTABIEL output. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public final class QbusBistabiel { + + private final Logger logger = LoggerFactory.getLogger(QbusBistabiel.class); + + @Nullable + private QbusCommunication QComm; + + private int id; + private Integer state = 0; + + @Nullable + private QbusBistabielHandler thingHandler; + + QbusBistabiel(int id) { + this.id = id; + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to this bistabiel is initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the bistable output receives an update from the Qbus IP-interface. + * + * @param handler + */ + public void setThingHandler(QbusBistabielHandler handler) { + this.thingHandler = handler; + } + + /** + * This method sets a pointer to the QComm BISTABIEL of class {@link QbusCommuncation}. + * This is then used to be able to call back the sendCommand method in this class to send a command to the + * Qbus IP-interface when.. + * + * @param QComm + */ + public void setQComm(QbusCommunication QComm) { + this.QComm = QComm; + } + + /** + * Get state of bistabiel. + * + * @return bistabiel state + */ + public Integer getState() { + return this.state; + } + + /** + * Sets state of bistabiel. + * + * @param bistabiel state + */ + void setState(int state) { + this.state = state; + QbusBistabielHandler handler = thingHandler; + if (handler != null) { + logger.debug("Qbus: update bistabiel channel state for {} with {}", id, state); + handler.handleStateUpdate(this); + } + } + + /** + * Sends bistabiel to Qbus. + */ + public void execute(int value, String sn) { + + logger.debug("Qbus: execute bistabiel for {} on CTD {}", this.id, sn); + + QMessageCmd QCmd = new QMessageCmd("executebistabiel", this.id, value).withSn(sn); + + QbusCommunication comm = QComm; + if (comm != null) { + comm.sendMessage(QCmd); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java new file mode 100644 index 0000000000000..b1aa025c16680 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java @@ -0,0 +1,75 @@ +/** + * 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.qbus.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusCO2Handler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusCO2} class represents the action Qbus CO2 output. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public final class QbusCO2 { + + private final Logger logger = LoggerFactory.getLogger(QbusCO2.class); + + private int id; + private Integer state = 0; + + @Nullable + private QbusCO2Handler thingHandler; + + QbusCO2(int id) { + this.id = id; + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to this CO2 is initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the CO2 output receives an update from the Qbus IP-interface. + * + * @param handler + */ + public void setThingHandler(QbusCO2Handler handler) { + this.thingHandler = handler; + } + + /** + * Get state of CO2. + * + * @return CO2 state + */ + public Integer getState() { + return this.state; + } + + /** + * Sets state of CO2. + * + * @param CO2 state + */ + public void setState(Integer co2) { + this.state = co2; + QbusCO2Handler handler = thingHandler; + if (handler != null) { + logger.debug("Qbus: update channel state for {} with {}", id, state); + handler.handleStateUpdate(this); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java new file mode 100644 index 0000000000000..47c2a115ffedc --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java @@ -0,0 +1,936 @@ +/** + * 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.qbus.internal.protocol; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.Socket; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; + +/** + * The {@link QbusCommunication} class is able to do the following tasks with Qbus + * systems: + *

    + *
  • Start and stop TCP socket connection with Qbus Server. + *
  • Read all setup and status information from the Qbus Controller. + *
  • Execute Qbus commands. + *
  • Listen to events from Qbus. + *
+ * + * A class instance is instantiated from the {@link QbusBridgeHandler} class initialization. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public final class QbusCommunication { + + private final Logger logger = LoggerFactory.getLogger(QbusCommunication.class); + + @Nullable + private Socket qSocket; + @Nullable + private PrintWriter qOut; + @Nullable + private BufferedReader qIn; + + private boolean listenerStopped; + private boolean qEventsRunning; + + private Gson gsonOut = new Gson(); + private Gson gsonIn; + + private final Map scenes = new HashMap<>(); + private final Map bistabiel = new HashMap<>(); + private final Map dimmer = new HashMap<>(); + private final Map thermostats = new HashMap<>(); + private final Map co2 = new HashMap<>(); + private final Map Rol = new HashMap<>(); + // private final Map Disconnect = new HashMap<>(); + + @Nullable + private QbusBridgeHandler bridgeCallBack; + + /** + * Constructor for Qbus communication object, manages communication with + * Qbus Server. + * + */ + public QbusCommunication() { + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(QbusMessageBase.class, new QbusMessageDeserializer()); + this.gsonIn = gsonBuilder.create(); + } + + /** + * Start communication with Qbus Server, run through initialization and start thread listening + * to all messages coming from Qbus. + * + * @param addr : IP-address of Qbus Server + * @param port : Communication port of Qbus server + * @param sn : Serial number of the controller + * + */ + public synchronized void startCommunication() { + QbusBridgeHandler handler = this.bridgeCallBack; + + try { + for (int i = 1; qEventsRunning && (i <= 5); i++) { + Thread.sleep(1000); + } + if (qEventsRunning) { + logger.error("Qbus: starting from thread {}, but previous connection still active after 5000ms", + Thread.currentThread().getId()); + throw new IOException(); + } + + if (handler == null) { + throw new IOException(); + } + + InetAddress addr = handler.getAddr(); + int port = handler.getPort(); + + Socket socket = new Socket(addr, port); + this.qSocket = socket; + this.qOut = new PrintWriter(socket.getOutputStream(), true); + this.qIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); + logger.debug("Qbus: connected via local port {} from thread {}", socket.getLocalPort(), + Thread.currentThread().getId()); + + initialize(); + + (new Thread(qEvents)).start(); + + } catch (IOException | InterruptedException e) { + logger.warn("Qbus: error initializing communication from thread {}", Thread.currentThread().getId()); + if (handler != null) { + handler.bridgeOffline(); + } + stopCommunication(); + + } + } + + /** + * Cleanup socket when the communication with Qbus Server is closed. + * + * @throws IOException + * + */ + public synchronized void stopCommunication() { + this.listenerStopped = true; + + Socket socket = this.qSocket; + + if (socket != null) { + try { + socket.close(); + } catch (IOException ignore) { + // ignore IO Error when trying to close the socket if the intention is to close it anyway + } + } + + this.qSocket = null; + // restartCommunication(); + logger.debug("Qbus: communication stopped from thread {}", Thread.currentThread().getId()); + } + + /** + * Close and restart communication with Qbus Server. + * + */ + public synchronized void restartCommunication() { + QbusBridgeHandler handler = this.bridgeCallBack; + stopCommunication(); + + // handler.bridgeOffline(); + + if (handler != null) { + handler.bridgeOffline(); + } + + logger.debug("Qbus: restart communication from thread {}", Thread.currentThread().getId()); + + startCommunication(); + } + + /** + * Method to check if communication with Qbus Server is active + * + * @return True if active + */ + public boolean communicationActive() { + return (this.qSocket != null); + } + + /** + * Runnable that handles inbound communication from Qbus server. + *

+ * The thread listens to the TCP socket opened at instantiation of the {@link QbusCommunication} class + * and interprets all inbound json messages. It triggers state updates for active channels linked to the + * Qbus outputs. It is started after initialization of the communication. + * + */ + private Runnable qEvents = () -> { + String qMessage; + // QbusBridgeHandler handler = this.bridgeCallBack; + + logger.debug("Qbus: listening for events on thread {}", Thread.currentThread().getId()); + listenerStopped = false; + qEventsRunning = true; + + BufferedReader reader = this.qIn; + + try { + if (reader == null) { + throw new IOException(); + } + while (!listenerStopped & ((qMessage = reader.readLine()) != null)) { + if (qMessage != null) { + readMessage(qMessage); + } + } + } catch (IOException e) { + if (!listenerStopped) { + qEventsRunning = false; + logger.warn("Qbus: IO error in listener on thread {}", Thread.currentThread().getId()); + restartCommunication(); + return; + } + } + + qEventsRunning = false; + + stopCommunication(); + + logger.debug("Qbus: event listener thread stopped on thread {}", Thread.currentThread().getId()); + + }; + + /** + * Method that interprets all feedback from Qbus Server application and calls appropriate handling methods. + * + * @param qMessage message read from Qbus. + */ + private void readMessage(String qMessage) { + logger.debug("Qbus: received json on thread {}", Thread.currentThread().getId()); + + try { + QbusMessageBase qMessageGson = this.gsonIn.fromJson(qMessage, QbusMessageBase.class); + String cmd = ""; + String event = ""; + + @SuppressWarnings("null") + String confsn = this.bridgeCallBack.getSn(); + String sn = qMessageGson.getSn(); + cmd = qMessageGson.getCmd(); + event = qMessageGson.getEvent(); + + // logger.debug(cmd); + // logger.debug(event); + + if (Integer.parseInt(confsn) == Integer.parseInt(sn)) { + // Get the compatible outputs from the Qbus server + if ("listbistabiel".equals(cmd)) { + cmdListBistabiel(((QMessageListMap) qMessageGson).getData()); + } else if ("listdimmers".equals(cmd)) { + cmdListDimmers(((QMessageListMap) qMessageGson).getData()); + } else if (("listthermostats").equals(cmd)) { + cmdListThermostat(((QMessageListMap) qMessageGson).getData()); + } else if (("listscenes").equals(cmd)) { + cmdlistscenes(((QMessageListMap) qMessageGson).getData()); + } else if (("listco2").equals(cmd)) { + cmdlistco2(((QMessageListMap) qMessageGson).getData()); + } else if (("listrol").equals(cmd)) { + cmdlistrol(((QMessageListMap) qMessageGson).getData()); + } else if (("listrol02pslats").equals(cmd)) { + cmdlistrolslats(((QMessageListMap) qMessageGson).getData()); + } + // Commands to execute from openHAB to Qbus + else if ("executebistabiel".equals(cmd)) { + cmdExecuteBistabiel(((QMessageMap) qMessageGson).getData()); + } else if ("executedimmers".equals(cmd)) { + cmdExecuteDimmer(((QMessageMap) qMessageGson).getData()); + } else if ("executethermostat".equals(cmd)) { + cmdExecuteThermostat(((QMessageMap) qMessageGson).getData()); + } else if ("executescene".equals(cmd)) { + cmdExecuteScene(((QMessageMap) qMessageGson).getData()); + } else if ("executeslats".equals(cmd)) { + cmdExecuteSlats(((QMessageMap) qMessageGson).getData()); + } else if ("executerol".equals(cmd)) { + cmdExecuteRol(((QMessageMap) qMessageGson).getData()); + } else if ("executerol02pslats".equals(cmd)) { + cmdExecuteRolslats(((QMessageMap) qMessageGson).getData()); + } + // Incoming commands from Qbus Server to openHAB (event) + else if ("listbistabiel".equals(event)) { + eventListBistabiel(((QMessageListMap) qMessageGson).getData()); + } else if ("listdimmers".equals(event)) { + eventListDimmers(((QMessageListMap) qMessageGson).getData()); + } else if ("listthermostat".equals(event)) { + eventListThermostat(((QMessageListMap) qMessageGson).getData()); + } else if ("listscenes".equals(event)) { + eventListScenes(((QMessageListMap) qMessageGson).getData()); + } else if ("listco2".equals(event)) { + eventListCO2(((QMessageListMap) qMessageGson).getData()); + } else if ("listrol".equals(event)) { + eventListRol(((QMessageListMap) qMessageGson).getData()); + } else if ("listrol02pslats".equals(event)) { + eventListRolslats(((QMessageListMap) qMessageGson).getData()); + } + // + else if ("disconnect".equals(event)) { + eventFunction(((QMessageListMap) qMessageGson).getData()); + } + } + } catch (JsonParseException e) { + logger.debug("Qbus: not acted on unsupported json {}", qMessage); + } + } + + /** + * After setting up the communication with the Qbus Server, send all initialization messages. + *

+ * First send connect to connect with the Qbus Server application + * Get request for the Scenes + * Get request for Bistabiel/Timers/Intervals/Mono outputs + * Get request for Dimmers 1T and 2T + * Get request for Thermostats + * Get request for CO2 + * Get request for Shutters + * + * @throws IOException + * @throws InterruptedException + */ + private void initialize() throws IOException, InterruptedException { + Connect(); + sendAndReadMessage("listrol"); + sendAndReadMessage("listrol02pslats"); + sendAndReadMessage("listscenes"); + sendAndReadMessage("listbistabiel"); + sendAndReadMessage("listdimmers"); + sendAndReadMessage("listthermostats"); + sendAndReadMessage("listco2"); + } + + /** + * Initial connection to Qbus Server to open a communication channel + */ + private void Connect() { + @SuppressWarnings("null") + String confsn = this.bridgeCallBack.getSn(); + QMessageCmd qCmd = new QMessageCmd("openHAB").withSn(confsn); + sendMessage(qCmd); + } + + /** + * Send message to Qbus server and read response + */ + private void sendAndReadMessage(String command) throws IOException, InterruptedException { + @SuppressWarnings("null") + String confsn = this.bridgeCallBack.getSn(); + QMessageCmd qCmd = new QMessageCmd(command).withSn(confsn); + + sendMessage(qCmd); + + BufferedReader reader = this.qIn; + if (reader == null) { + throw new IOException("Cannot read from socket, reader not connected."); + } + readMessage(reader.readLine()); + } + + /** + * Get all the scenes from the Qbus server + * + * @param data + */ + private void cmdlistscenes(@Nullable List> data) { + logger.debug("Qbus: Scenes received from Qbus server"); + + if (data != null) { + for (Map scene : data) { + try { + int id = Integer.parseInt(scene.get("id")); + QbusScene Scene = new QbusScene(id); + Scene.setQComm(this); + this.scenes.put(id, Scene); + } catch (Exception e) { + logger.debug("Qbus: Error in json for Scenes"); + } + } + } + } + + /** + * Get all the CO2 outputs from the Qbus server + * + * @param data + */ + private void cmdlistco2(@Nullable List> data) { + logger.debug("Qbus: CO2 received from Qbus server"); + + if (data != null) { + for (Map co2 : data) { + + try { + int id = Integer.parseInt(co2.get("id")); + Integer co = 0; + co = Integer.parseInt(co2.get("value1")); + if (!this.co2.containsKey(id)) { + QbusCO2 CO2 = new QbusCO2(id); + this.co2.put(id, CO2); + this.co2.get(id).setState(co); + } else { + this.co2.get(id).setState(co); + } + } catch (Exception e) { + logger.debug("Qbus: Error in json for CO2"); + } + + } + } + } + + /** + * Get all the Positioning module outputs from the Qbus server + * + * @param data + */ + private void cmdlistrol(@Nullable List> data) { + logger.debug("Qbus: ROL02P received from Qbus server"); + + if (data != null) { + for (Map rol : data) { + int id = 0; + id = Integer.parseInt(rol.get("id")); + Integer rolpos = 0; + // Integer rolposslats = 0; + try { + rolpos = Integer.valueOf(rol.get("value1")); + // rolposslats = Integer.valueOf(rol.get("value2")); + } catch (Exception e) { + logger.debug("Qbus: Error in json for Rollershutter"); + } + + if (!this.Rol.containsKey(id)) { + QbusRol Rol = new QbusRol(id); + Rol.setQComm(this); + this.Rol.put(id, Rol); + this.Rol.get(id).setState(rolpos); + // this.Rol.get(id).setSlats(rolposslats); + } else { + this.Rol.get(id).setState(rolpos); + // this.Rol.get(id).setSlats(rolposslats); + } + } + } + } + + /** + * Get all the Positioning module outputs from the Qbus server + * + * @param data + */ + private void cmdlistrolslats(@Nullable List> data) { + logger.debug("Qbus: ROL02PSLATS received from Qbus server"); + + if (data != null) { + for (Map rol : data) { + int id = 0; + id = Integer.parseInt(rol.get("id")); + Integer rolpos = 0; + Integer rolposslats = 0; + try { + rolpos = Integer.valueOf(rol.get("value1")); + rolposslats = Integer.valueOf(rol.get("value2")); + } catch (Exception e) { + logger.debug("Qbus: Error in json for Rollershutter"); + } + + if (!this.Rol.containsKey(id)) { + QbusRol Rol = new QbusRol(id); + Rol.setQComm(this); + this.Rol.put(id, Rol); + this.Rol.get(id).setState(rolpos); + this.Rol.get(id).setSlats(rolposslats); + } else { + this.Rol.get(id).setState(rolpos); + this.Rol.get(id).setSlats(rolposslats); + } + } + } + } + + /** + * Get all the Bistabiel/Timers/Mono/Interval from the Qbus server + * + * @param data + */ + private void cmdListBistabiel(@Nullable List> data) { + logger.debug("Qbus: Bistabiel/Timers/Monos/Intervals received from Qbus server"); + + if (data != null) { + for (Map bistabiel : data) { + int id = 0; + int state = 0; + id = Integer.parseInt(bistabiel.get("id")); + try { + state = Integer.parseInt(bistabiel.get("value1")); + } catch (Exception e) { + logger.debug("Qbus: Error in json for Bistabiel"); + } + + if (!this.bistabiel.containsKey(id)) { + QbusBistabiel qBistabiel = new QbusBistabiel(id); + qBistabiel.setState(state); + qBistabiel.setQComm(this); + this.bistabiel.put(id, qBistabiel); + } else { + this.bistabiel.get(id).setState(state); + } + } + } + } + + /** + * Get all the Dimmer outputs from the Qbus server + * + * @param data + */ + private void cmdListDimmers(@Nullable List> data) { + logger.debug("Qbus: Dimmers received from the Qbus server"); + + if (data != null) { + for (Map dimmer : data) { + + int id = 0; + int state = 0; + id = Integer.parseInt(dimmer.get("id")); + try { + state = Integer.parseInt(dimmer.get("value1")); + } catch (Exception e) { + logger.debug("Qbus: Error in json for Dimmer"); + } + + if (!this.dimmer.containsKey(id)) { + QbusDimmer qDimmer = new QbusDimmer(id); + qDimmer.setState(state); + qDimmer.setQComm(this); + this.dimmer.put(id, qDimmer); + } else { + this.dimmer.get(id).setState(state); + } + } + } + } + + /** + * Get all the Thermostat outputs from the Qbus server + * + * @param data + */ + private void cmdListThermostat(@Nullable List> data) { + logger.debug("Qbus: thermostats received from the Qbus server"); + + if (data != null) { + for (Map thermostat : data) { + int id = Integer.parseInt(thermostat.get("id")); + Double measured = 0.0; + Double setpoint = 0.0; + int mode = 0; + try { + measured = Double.valueOf(thermostat.get("measured")); + setpoint = Double.valueOf(thermostat.get("setpoint")); + mode = Integer.valueOf(thermostat.get("mode")); + } catch (Exception e) { + logger.debug("Qbus: Error in json for Thermostat"); + } + + if (!this.thermostats.containsKey(id)) { + QThermostat qThermostat = new QThermostat(id); + qThermostat.updateState(measured, setpoint, mode); + qThermostat.setQComm(this); + this.thermostats.put(id, qThermostat); + } else { + this.thermostats.get(id).updateState(measured, setpoint, mode); + } + } + } + } + + /** + * Execute Bistabiel/Timers/Monos/Intervals + * + * @param data + */ + private void cmdExecuteBistabiel(Map data) { + Integer errorCode = Integer.valueOf(data.get("error")); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute bistabiel success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } + } + + /** + * Execute Scenes + * + * @param data + */ + private void cmdExecuteScene(Map data) { + Integer errorCode = Integer.valueOf(data.get("error")); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute scene success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } + } + + /** + * Execute Dimmers + * + * @param data + */ + private void cmdExecuteDimmer(Map data) { + Integer errorCode = Integer.valueOf(data.get("error")); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute dimmer success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } + } + + /** + * Execute Slats + * + * @param data + */ + private void cmdExecuteSlats(Map data) { + Integer errorCode = Integer.valueOf(data.get("error")); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute slats success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } + } + + /** + * Execute Shutter + * + * @param data + */ + private void cmdExecuteRol(Map data) { + Integer errorCode = Integer.valueOf(data.get("error")); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute shutter success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } + } + + /** + * Execute Shutter with slats + * + * @param data + */ + private void cmdExecuteRolslats(Map data) { + Integer errorCode = Integer.valueOf(data.get("error")); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute shutter with slats success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } + } + + /** + * Execute Thermostats + * + * @param data + */ + private void cmdExecuteThermostat(Map data) { + Integer errorCode = Integer.valueOf(data.get("error")); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute thermostat success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } + } + + /** + * Event on incomming Bistabiel/Timer/Mono/Interval updates + * + * @param data + */ + private void eventListBistabiel(List> data) { + for (Map bistabiel : data) { + int id = Integer.valueOf(bistabiel.get("id")); + if (!this.bistabiel.containsKey(id)) { + logger.warn("Qbus: bistabiel in controller not known {}", id); + return; + } + Integer state = Integer.valueOf(bistabiel.get("value1")); + logger.debug("Qbus: event execute bistabiel {} with state {}", id, state); + this.bistabiel.get(id).setState(state); + } + } + + /** + * Event on incomming CO2 updates + * + * @param data + */ + private void eventListCO2(List> data) { + for (Map co2 : data) { + int id = Integer.valueOf(co2.get("id")); + if (!this.co2.containsKey(id)) { + logger.warn("Qbus: co2 in controller not known {}", id); + return; + } + Integer state = Integer.valueOf(co2.get("value1")); + logger.debug("Qbus: event execute co2 {} with state {}", id, state); + this.co2.get(id).setState(state); + } + } + + /** + * Event on incomming ROL02P without updates + * + * @param data + */ + private void eventListRol(List> data) { + for (Map rol : data) { + int id = Integer.valueOf(rol.get("id")); + if (!this.Rol.containsKey(id)) { + logger.warn("Qbus: Rol02p in controller not known {}", id); + return; + } + Integer pos = Integer.valueOf(rol.get("pos")); + // Integer slat = Integer.valueOf(rol.get("slats")); + logger.debug("Qbus: event execute Rol02P {} with pos {}", id, pos); + this.Rol.get(id).setState(pos); + // this.Rol.get(id).setSlats(slat); + } + } + + /** + * Event on incomming ROL02P with slats updates + * + * @param data + */ + private void eventListRolslats(List> data) { + for (Map rol : data) { + int id = Integer.valueOf(rol.get("id")); + if (!this.Rol.containsKey(id)) { + logger.warn("Qbus: Rol02p in controller not known {}", id); + return; + } + Integer pos = Integer.valueOf(rol.get("pos")); + Integer slat = Integer.valueOf(rol.get("slats")); + logger.debug("Qbus: event execute ROL02P_Slats {} with pos {} and slats {}", id, pos, slat); + this.Rol.get(id).setState(pos); + this.Rol.get(id).setSlats(slat); + } + } + + /** + * Event on incomming Scene updates + * + * @param data + */ + private void eventListScenes(List> data) { + for (Map scene : data) { + int id = Integer.valueOf(scene.get("id")); + if (!this.scenes.containsKey(id)) { + logger.warn("Qbus: scene in controller not known {}", id); + return; + } + Integer state = Integer.valueOf(scene.get("value1")); + logger.debug("Qbus: event execute scene {} with state {}", id, state); + this.scenes.get(id).setState(state); + } + } + + /** + * Event on incomming Dimmer updates + * + * @param data + */ + private void eventListDimmers(List> data) { + for (Map dimmer : data) { + int id = Integer.valueOf(dimmer.get("id")); + if (!this.dimmer.containsKey(id)) { + logger.warn("Qbus: dimmer in controller not known {}", id); + return; + } + Integer state = Integer.valueOf(dimmer.get("value1")); + logger.debug("Qbus: event execute dimmer {} with state {}", id, state); + this.dimmer.get(id).setState(state); + } + } + + /** + * Event on incomming thermostat updates + * + * @param data + */ + private void eventListThermostat(List> data) { + for (Map thermostat : data) { + int id = Integer.parseInt(thermostat.get("id")); + if (!this.thermostats.containsKey(id)) { + logger.warn("Qbus: thermostat in controller not known {}", id); + return; + } + Double measured = Double.valueOf(thermostat.get("measured")); + Double setpoint = Double.valueOf(thermostat.get("setpoint")); + Integer mode = Integer.valueOf(thermostat.get("mode")); + logger.debug("Qbus: event execute thermostat {} with measured {}, setpoint {}, mode {}", id, measured, + setpoint, mode); + this.thermostats.get(id).updateState(measured, setpoint, mode); + } + } + + /* + * /** + * Event on Disconnect + * + * @param data + */ + private void eventFunction(List> data) { + // for (Map function : data) { + logger.debug("Disconnect"); + // String ctd = function.get("ctd"); + // String funcion = function.get("function"); + + QbusBridgeHandler handler = this.bridgeCallBack; + + if (handler != null) { + stopCommunication(); + handler.bridgeOffline(); + + } + } + + /** + * Called by other methods to send json cmd to Qbus. + * + * @param qMessage + */ + synchronized void sendMessage(Object qMessage) { + PrintWriter writer = this.qOut; + String json = gsonOut.toJson(qMessage); + logger.debug("Qbus: send json from thread {}", Thread.currentThread().getId()); + + if (writer != null) { + writer.println(json); + + try { + TimeUnit.MILLISECONDS.sleep(250); + } catch (InterruptedException e) { + // No reaction on error is required + } + + } + if ((writer == null) || (writer.checkError())) { + logger.warn("Qbus: error sending message, trying to restart communication"); + restartCommunication(); + // retry sending after restart + logger.debug("Qbus: resend json from thread {}", Thread.currentThread().getId()); + writer = this.qOut; + if (writer != null) { + writer.println(json); + } + if ((writer == null) || (writer.checkError())) { + logger.warn("Qbus: error resending message"); + + } + } + } + + /** + * Return all Bistabiel/Timers/Mono/Intervals in the Qbus Controller. + * + * @return + */ + public Map getBistabiel() { + return this.bistabiel; + } + + /** + * Return all Dimmers in the Qbus Controller. + * + * @return + */ + public Map getDimmer() { + return this.dimmer; + } + + /** + * Return all Scenes in the Qbus Controller + * + * @return + */ + public Map getScenes() { + return this.scenes; + } + + /** + * Return all Thermostats in the Qbus Controller. + * + * @return + */ + public Map getThermostats() { + return this.thermostats; + } + + /** + * Return all CO2 in the Qbus Controller. + * + * @return + */ + public Map getCo2() { + return this.co2; + } + + /** + * Return all ROL02P outûts in the Qbus Controller. + * + * @return + */ + public Map getRol() { + return this.Rol; + } + + /** + * @param bridgeCallBack the bridgeCallBack to set + */ + public void setBridgeCallBack(QbusBridgeHandler bridgeCallBack) { + this.bridgeCallBack = bridgeCallBack; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java new file mode 100644 index 0000000000000..b7fb67c1ef84f --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.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.qbus.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusDimmerHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusDimmer} class represents the action Qbus Dimmer output. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public final class QbusDimmer { + + private final Logger logger = LoggerFactory.getLogger(QbusDimmer.class); + + @Nullable + private QbusCommunication QComm; + + private int id; + private Integer state = 0; + + @Nullable + private QbusDimmerHandler thingHandler; + + QbusDimmer(int id) { + this.id = id; + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to this dimmer is initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the dimmer receives an update from the Qbus IP-interface. + * + * @param handler + */ + public void setThingHandler(QbusDimmerHandler handler) { + this.thingHandler = handler; + } + + /** + * This method sets a pointer to the QComm Dimmer of class {@link QbusCommuncation}. + * This is then used to be able to call back the sendCommand method in this class to send a command to the + * Qbus IP-interface when.. + * + * @param QComm + */ + public void setQComm(QbusCommunication QComm) { + this.QComm = QComm; + } + + /** + * Get state of dimmer. + * + * @return dimmer state + */ + public Integer getState() { + return this.state; + } + + /** + * Sets state of Dimmer. + * + * @param dimmer state + */ + void setState(int state) { + this.state = state; + QbusDimmerHandler handler = thingHandler; + if (handler != null) { + logger.debug("Qbus: update channel state for {} with {}", id, state); + handler.handleStateUpdate(this); + } + } + + /** + * Sends Dimmer state to Qbus. + */ + public void execute(int percent, String sn) { + logger.debug("Qbus: execute dimmer for {} on CTD {}", this.id, sn); + + QMessageCmd QCmd = new QMessageCmd("executedimmer", this.id, percent).withSn(sn); + + QbusCommunication comm = QComm; + if (comm != null) { + comm.sendMessage(QCmd); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java new file mode 100644 index 0000000000000..6b6b1bf71900a --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.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.qbus.internal.protocol; + +/** + * Class {@link QbusMessageBase} used as base class for output from gson for cmd or event feedback from the Qbus server. + * This class only contains the common base fields required for the deserializer + * {@link QbusMessageDeserializer} to select the specific formats implemented in {@link QbusMessageMap}, + * {@link QbusMessageListMap}, {@link QbusMessageCmd}. + *

+ * + * @author Koen Schockaert - Initial Contribution + */ + +abstract class QbusMessageBase { + + private String cmd = ""; + private String event1 = ""; + private String sn = ""; + + String getCmd() { + return this.cmd; + } + + void setCmd(String cmd) { + this.cmd = cmd; + } + + void setSn(String sn) { + this.sn = sn; + } + + String getEvent() { + return this.event1; + } + + void setEvent(String event) { + this.event1 = event; + } + + String getSn() { + return this.sn; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java new file mode 100644 index 0000000000000..eee52cd2fd3ca --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java @@ -0,0 +1,110 @@ +/** + * 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.qbus.internal.protocol; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * Class {@link QbusMessageDeserializer} deserializes all json messages from Qbus. Various json + * message formats are supported. The format is selected based on the content of the cmd and event json objects. + * + * @author Koen Schockaert - Initial Contribution + * + */ + +class QbusMessageDeserializer implements JsonDeserializer { + + @Override + public QbusMessageBase deserialize(final JsonElement json, final Type typeOfT, + final JsonDeserializationContext context) throws JsonParseException { + final JsonObject jsonObject = json.getAsJsonObject(); + + try { + + String cmd = null; + String event1 = null; + String sn = null; + + if (jsonObject.has("cmd")) { + cmd = jsonObject.get("cmd").getAsString(); + } + if (jsonObject.has("event1")) { + event1 = jsonObject.get("event1").getAsString(); + } + if (jsonObject.has("ctdsn")) { + sn = jsonObject.get("ctdsn").getAsString(); + } + + JsonElement jsonData = null; + if (jsonObject.has("data")) { + jsonData = jsonObject.get("data"); + } + + QbusMessageBase message = null; + + if (jsonData != null) { + if (jsonData.isJsonObject()) { + message = new QMessageMap(); + + Map data = new HashMap<>(); + for (Entry entry : jsonData.getAsJsonObject().entrySet()) { + data.put(entry.getKey(), entry.getValue().getAsString()); + } + ((QMessageMap) message).setData(data); + + } else if (jsonData.isJsonArray()) { + JsonArray jsonDataArray = jsonData.getAsJsonArray(); + + message = new QMessageListMap(); + + List> dataList = new ArrayList<>(); + for (int i = 0; i < jsonDataArray.size(); i++) { + JsonObject jsonDataObject = jsonDataArray.get(i).getAsJsonObject(); + + Map data = new HashMap<>(); + for (Entry entry : jsonDataObject.entrySet()) { + data.put(entry.getKey(), entry.getValue().getAsString()); + } + dataList.add(data); + } + ((QMessageListMap) message).setData(dataList); + } + } + + if (message != null) { + message.setCmd(cmd); + message.setEvent(event1); + message.setSn(sn); + } else { + throw new JsonParseException("Unexpected Json type"); + } + + return message; + + } catch (IllegalStateException | ClassCastException e) { + throw new JsonParseException("Unexpected Json type"); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java new file mode 100644 index 0000000000000..29d4cb6779c41 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java @@ -0,0 +1,141 @@ +/** + * 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.qbus.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusRolHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusRol} class represents the action Qbus Shutter/Slats output. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public final class QbusRol { + + private final Logger logger = LoggerFactory.getLogger(QbusRol.class); + + @Nullable + private QbusCommunication QComm; + + private int id; + private Integer state = 0; + private Integer slats = 0; + + @Nullable + private QbusRolHandler thingHandler; + + QbusRol(int id) { + this.id = id; + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to this Shutter/Slats is + * initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the shutter/slat receives an update from the Qbus IP-interface. + * + * @param qbusRolHandler + */ + public void setThingHandler(QbusRolHandler qbusRolHandler) { + this.thingHandler = qbusRolHandler; + } + + /** + * This method sets a pointer to the QComm Shutter/Slats of class {@link QbusCommuncation}. + * This is then used to be able to call back the sendCommand method in this class to send a command to the + * Qbus IP-interface when.. + * + * @param QComm + */ + public void setQComm(QbusCommunication QComm) { + this.QComm = QComm; + } + + /** + * Get state of shutter. + * + * @return shutter state + */ + public Integer getState() { + return this.state; + } + + /** + * Get state of slats. + * + * @return slats state + */ + public Integer getStateSlats() { + return this.slats; + } + + /** + * Sets state of Shutter. + * + * @param shutter state + */ + public void setState(Integer Slats) { + this.state = Slats; + QbusRolHandler handler = thingHandler; + if (handler != null) { + logger.debug("Qbus: update channel shutter for {} with {}", id, state); + handler.handleStateUpdate(this); + } + } + + /** + * Sets state of Slats. + * + * @param slats state + */ + public void setSlats(Integer Slats) { + this.slats = Slats; + QbusRolHandler handler = thingHandler; + if (handler != null) { + logger.debug("Qbus: update channel slats for {} with {}", id, slats); + handler.handleStateUpdate(this); + } + } + + /** + * Sends shutter to Qbus. + */ + public void execute(int percent, String sn) { + logger.debug("Qbus: execute position for {} {}", this.id, sn); + + QMessageCmd QCmd = new QMessageCmd("executestore", this.id, percent).withSn(sn); + + QbusCommunication comm = QComm; + if (comm != null) { + comm.sendMessage(QCmd); + } + } + + /** + * Sends slats to Qbus. + */ + public void executeSlats(int percent, String sn) { + logger.debug("Qbus: execute slats for {} {}", this.id, sn); + + QMessageCmd QCmd = new QMessageCmd("executeslats", this.id, percent).withSn(sn); + + QbusCommunication comm = QComm; + if (comm != null) { + comm.sendMessage(QCmd); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java new file mode 100644 index 0000000000000..6b58b37da3157 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java @@ -0,0 +1,103 @@ +/** + * 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.qbus.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusSceneHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusScene} class represents the action Qbus Scene output. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public final class QbusScene { + + private final Logger logger = LoggerFactory.getLogger(QbusScene.class); + + @Nullable + private QbusCommunication QComm; + + private int id; + private Integer state = 0; + + @Nullable + private QbusSceneHandler thingHandler; + + QbusScene(int id) { + this.id = id; + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to this scene is initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the scene receives an update from the Qbus IP-interface. + * + * @param handler + */ + public void setThingHandler(QbusSceneHandler handler) { + this.thingHandler = handler; + } + + /** + * This method sets a pointer to the QComm Scene of class {@link QbusCommuncation}. + * This is then used to be able to call back the sendCommand method in this class to send a command to the + * Qbus IP-interface when.. + * + * @param QComm + */ + public void setQComm(QbusCommunication QComm) { + this.QComm = QComm; + } + + /** + * Get state of scene. + * + * @return scene state + */ + public Integer getState() { + return this.state; + } + + /** + * Sets state of Scene. + * + * @param scene state + */ + void setState(int state) { + this.state = state; + QbusSceneHandler handler = thingHandler; + if (handler != null) { + logger.debug("Qbus: update channel state for {} with {}", id, state); + handler.handleStateUpdate(this); + } + } + + /** + * Sends action to Qbus. + */ + public void execute(int val, String sn) { + logger.debug("Qbus: execute scene for {} on CTD {}", this.id, sn); + + QMessageCmd QCmd = new QMessageCmd("executescene", this.id, val).withSn(sn); + ; + + QbusCommunication comm = QComm; + if (comm != null) { + comm.sendMessage(QCmd); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..b18daa90afc8d --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + Qbus Binding + This is the binding for Qbus. + Koen Schockaert + + diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties new file mode 100644 index 0000000000000..0c7193b3f16c7 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties @@ -0,0 +1,17 @@ +# FIXME: please substitute the xx_XX with a proper locale, ie. de_DE +# FIXME: please do not add the file to the repo if you add or change no content +# binding +binding.qbus.name = +binding.qbus.description = + +# thing types +thing-type.qbus.sample.label = +thing-type.qbus.sample.description = + +# thing type config description +thing-type.config.qbus.sample.config1.label = +thing-type.config.qbus.sample.config1.description = + +# channel types +channel-type.qbus.sample-channel.label = +channel-type.qbus.sample-channel.description = diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..a7aef153dc7e5 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,247 @@ + + + + + + This bridge represents a Qbus client + + + + IP Address of Qbus server, usually 'localhost' + localhost + false + network-address + + + + Serial nr of the CTD controller + + false + + + + Port to communicate with Qbus server, default 8447 + 8447 + true + + + + Refresh interval for connection with Qbus server (min), default 300. If set to 0 or left empty, no + refresh will be scheduled + 300 + true + + + + + + + + + + Bistabiel-Mono-Timer-Interval output + + + + + + + Qbus IP Interface Output Object ID + false + + + + + + + + + + Qbus scene + + + + + + + Qbus scene ID + false + + + + + + + + + + + Qbus CO2 + + + + + + + Qbus CO2 ID + false + + + + + + + + + + Qbus dimmer output + + + + + + + Qbus IP Interface Output ID + false + + + + Step value used for increase/decrease of dimmer brightness, default 10% + 10 + true + + + + + + + + + + + + Qbus Shutter control + + + + + + + + Qbus rol Id + false + + + + + + + + + + Qbus thermostat + + + + + + + + + Qbus IP Interface Thermostat ID + false + + + + + + Switch + + Scene control for Qbus + Scene + + + + Number + + CO2 value for Qbus + CO2 + + + + Switch + + Switch control for output in Qbus + Switch + + + + Dimmer + + Brightness control for dimmer function in Qbus + DimmableLight + + + + Color + + DMX control for dimmer function in Qbus + HSB + + + + Number:Temperature + + Temperature measured by thermostat + Temperature + + CurrentTemperature + + + + + + Number:Temperature + + Setpoint temperature of thermostat + Temperature + + TargetTemperature + + + + + + Number + + Thermostat mode + Number + + + + + + + + + + + + + Rollershutter + + Rollershutter control for Qbus + Blinds + + + + Dimmer + + Slatcontrol function in Qbus + Blinds + + + From f41c02e56b11a6c57711d28655c9aa425a8b4615 Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Mon, 30 Nov 2020 10:28:16 +0100 Subject: [PATCH 02/17] Revert "First commit" This reverts commit da86aae5e99beb06698150ded1d500529498b44f. Signed-off-by: QbusKoen --- bundles/org.openhab.binding.qbus/NOTICE | 13 - bundles/org.openhab.binding.qbus/README.md | 103 -- bundles/org.openhab.binding.qbus/doc/Logo.JPG | Bin 12981 -> 0 bytes bundles/org.openhab.binding.qbus/pom.xml | 17 - .../src/main/feature/feature.xml | 23 - .../qbus/internal/QbusBindingConstants.java | 98 -- .../qbus/internal/QbusBridgeHandler.java | 241 ----- .../qbus/internal/QbusHandlerFactory.java | 69 -- .../handler/QbusBistabielHandler.java | 183 ---- .../qbus/internal/handler/QbusCO2Handler.java | 167 ---- .../internal/handler/QbusDimmerHandler.java | 250 ----- .../qbus/internal/handler/QbusRolHandler.java | 264 ----- .../internal/handler/QbusSceneHandler.java | 181 ---- .../handler/QbusThermostatHandler.java | 183 ---- .../qbus/internal/protocol/QMessageCmd.java | 75 -- .../internal/protocol/QMessageListMap.java | 38 - .../qbus/internal/protocol/QMessageMap.java | 36 - .../qbus/internal/protocol/QThermostat.java | 175 ---- .../qbus/internal/protocol/QbusBistabiel.java | 103 -- .../qbus/internal/protocol/QbusCO2.java | 75 -- .../internal/protocol/QbusCommunication.java | 936 ------------------ .../qbus/internal/protocol/QbusDimmer.java | 102 -- .../internal/protocol/QbusMessageBase.java | 54 - .../protocol/QbusMessageDeserializer.java | 110 -- .../qbus/internal/protocol/QbusRol.java | 141 --- .../qbus/internal/protocol/QbusScene.java | 103 -- .../main/resources/OH-INF/binding/binding.xml | 10 - .../OH-INF/i18n/qbus_xx_XX.properties | 17 - .../resources/OH-INF/thing/thing-types.xml | 247 ----- 29 files changed, 4014 deletions(-) delete mode 100644 bundles/org.openhab.binding.qbus/NOTICE delete mode 100644 bundles/org.openhab.binding.qbus/README.md delete mode 100644 bundles/org.openhab.binding.qbus/doc/Logo.JPG delete mode 100644 bundles/org.openhab.binding.qbus/pom.xml delete mode 100644 bundles/org.openhab.binding.qbus/src/main/feature/feature.xml delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageListMap.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageMap.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QThermostat.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml delete mode 100644 bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties delete mode 100644 bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml diff --git a/bundles/org.openhab.binding.qbus/NOTICE b/bundles/org.openhab.binding.qbus/NOTICE deleted file mode 100644 index 38d625e349232..0000000000000 --- a/bundles/org.openhab.binding.qbus/NOTICE +++ /dev/null @@ -1,13 +0,0 @@ -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.qbus/README.md b/bundles/org.openhab.binding.qbus/README.md deleted file mode 100644 index 89b5efe07d84d..0000000000000 --- a/bundles/org.openhab.binding.qbus/README.md +++ /dev/null @@ -1,103 +0,0 @@ -# Qbus Binding -![Qbus Logo](doc/Logo.JPG) - -This binding for Qbus communicates for all controllers of the Qbus home automation system. - -More information about Qbus can be found here: -[Qbus](https://qbus.be) - -This is the link to the [Qbus forum](https://qbusforum.be). This forum is mainly in dutch and you can find a lot of information about the pre testings af this binding and offers a way to communicate with other users. - -We also host a site which contains a [manual](https://manualoh.schockaert.tk/) where you can find lot's of information. - -The controllers can not communicate directly with openHAB, therefore we developed a Client/Server application which you must install prior to enable this binding. -More information can be found here: -[Qbus Client/Server](https://github.com/QbusKoen/QbusClientServer) - -With this binding you can control and read almost every output from the Qbus system. - -## Supported Things - -The following things are supported by the Qbus Binding: -- Dimmer 1 button, 2 button and clc as _dimmer_ -- Bistabiel, Timer1-3, Interval as _onOff_ -- Thermostats - normal and PID as _thermosats_ -- Scenes as _scene_ -- CO2 as _co2_ -- Rollershutter and rollerhutter with slats as _rollershutter_ - -For now the folowing Qbus things are not yet supported but will come: -- DMX -- Timer 4 & 5 -- HVAC -- Humidity -- Renson -- Duco -- Kinetura -- Energy monitor -- Weather station - - -## Discovery - -The discovery service is not yet implemented but the System Manager III software of Qbus generates things and item files from the programming, which you can use directly in openHAB. - -## Thing Configuration - -This is an example of the things configuration file: - -``` -Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, refresh=10 ] { - dimmer 1 "ToonzaalLED" [ dimmerId=100 ] - onOff 30 "Toonzaal230V" [ bistabielId=76 ] - thermostat 50 "Service" [ thermostatId=99 ] - scene 70 "Disco" [ sceneId=36 ] - co2 100 "Productie" [ co2Id=26 ] - rollershutter 120 "Roller1" [ rolId=268 ] - rollershutter_slats 121 "Roller2" [ rolId=264 ] -} -``` -The Bridge connects to the QbusServer, so if the Client/Server application is installed on the same machine then localhost can be used as address. sn is the serial nr of your controller, port should allways be 8447, except in special installations as it is the communication port of the Server application. refresh is a time in minutes which will check the status of the server and reconnects if connection is broken after this time. - -## Channels - -| channel | type | description | -|---------------------|---------------|---------------------------------------------------------| -| onOff | switch | This is the channel for Bistable, Timers and Intervals | -| dimmer | brightness | This is the channel for Dimmers 1&2 buttons and CLC | -| scene | Switch | This is the channel for scenes | -| co2 | co2 | This is the channel for CO2 sensors | -| rollershutter | rollershutter | This is the channel for rollershutters | -| rollershutter_slats | rollershutter | This is the channel for rollershutters with slats | -| thermostat | setpoint | This is the channel for thermostats setpoint | -| thermostat | measured | This is the channel for thermostats currenttemp | -| thermostat | mode | This is the channel for thermostats mode | - - -## Full Example - -### Things: -``` -Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, refresh=10 ] { - dimmer 1 "ToonzaalLED" [ dimmerId=100 ] - onOff 30 "Toonzaal230V" [ bistabielId=76 ] - thermostat 50 "Service" [ thermostatId=99 ] - scene 70 "Disco" [ sceneId=36 ] - co2 100 "Productie" [ co2Id=26 ] - rollershutter 120 "Roller1" [ rolId=268 ] - rollershutter_slats 121 "Roller2" [ rolId=264 ] -} -``` -### Items: -``` -Dimmer ToonzaalLED [ "Lighting" ] {channel="qbus:dimmer:CTD007841:1:brightness"} -Switch Toonzaal230V {channel="qbus:onOff:CTD007841:30:switch"} -Number:Temperature ServiceSP"[%.1f °C]" (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:setpoint"} -Number:Temperature ServiceCT"[%.1f °C]" (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:measured"} -Number ServiceMode (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:mode",ihc="0x33c311" , autoupdate="true"} -Switch Disco {channel="qbus:scene:CTD007841:36:scene"} -Number ProductieCO2 {channel="qbus:co2:CTD007841:100:co2"} -Rollershutter Roller1 {channel="qbus:rollershutter:CTD007841:120:rollershutter"} -Rollershutter Roller2 {channel="qbus:rollershutter_slats:CTD007841:121:rollershutter"} -Dimmer Roller2_slats {channel="qbus:rollershutter_slats:CTD007841:121:slats"} -``` \ No newline at end of file diff --git a/bundles/org.openhab.binding.qbus/doc/Logo.JPG b/bundles/org.openhab.binding.qbus/doc/Logo.JPG deleted file mode 100644 index 4db5d75ae4403d9d1aa945287c333e9dc85d6130..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12981 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 diff --git a/bundles/org.openhab.binding.qbus/pom.xml b/bundles/org.openhab.binding.qbus/pom.xml deleted file mode 100644 index 3cda2c4358f52..0000000000000 --- a/bundles/org.openhab.binding.qbus/pom.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - 4.0.0 - - - org.openhab.addons.bundles - org.openhab.addons.reactor.bundles - 3.0.0-SNAPSHOT - - - org.openhab.binding.qbus - - openHAB Add-ons :: Bundles :: Qbus Binding - - diff --git a/bundles/org.openhab.binding.qbus/src/main/feature/feature.xml b/bundles/org.openhab.binding.qbus/src/main/feature/feature.xml deleted file mode 100644 index fcae4ee3aee20..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/feature/feature.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - 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.qbus/${project.version} - - diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java deleted file mode 100644 index 5cd8e6bc2b10f..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java +++ /dev/null @@ -1,98 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal; - -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.openhab.core.thing.ThingTypeUID; - -/** - * The {@link QbusBindingConstants} class defines common constants, which are - * used across the whole binding. - * - * @author Koen Schockaert - Initial contribution - */ -@NonNullByDefault -public class QbusBindingConstants { - - private static final String BINDING_ID = "qbus"; - - // bridge - public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "bridge"); - public static final Set BRIDGE_THING_TYPES_UIDS = Collections.singleton(BRIDGE_THING_TYPE); - // Bridge config properties - public static final String CONFIG_HOST_NAME = "addr"; - public static final String CONFIG_PORT = "port"; - public static final String CONFIG_REFRESH = "refresh"; - public static final String CONFIG_SN = "sn"; - - // generic thing types - public static final ThingTypeUID THING_TYPE_CO2 = new ThingTypeUID(BINDING_ID, "co2"); - public static final ThingTypeUID THING_TYPE_SCENE = new ThingTypeUID(BINDING_ID, "scene"); - public static final ThingTypeUID THING_TYPE_TIMER_LIGHT = new ThingTypeUID(BINDING_ID, "onOff"); - public static final ThingTypeUID THING_TYPE_ON_OFF_LIGHT = new ThingTypeUID(BINDING_ID, "onOff"); - public static final ThingTypeUID THING_TYPE_DIMMABLE_LIGHT = new ThingTypeUID(BINDING_ID, "dimmer"); - public static final ThingTypeUID THING_TYPE_ROLLERSHUTTER = new ThingTypeUID(BINDING_ID, "rollershutter"); - public static final ThingTypeUID THING_TYPE_ROLLERSHUTTER_SLATS = new ThingTypeUID(BINDING_ID, - "rollershutter_slats"); - public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat"); - // List of all Thing Type UIDs - public static final Set SCENE_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(THING_TYPE_SCENE).collect(Collectors.toSet())); - - public static final Set CO2_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(THING_TYPE_CO2).collect(Collectors.toSet())); - - public static final Set ROLLERSHUTTER_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(THING_TYPE_ROLLERSHUTTER).collect(Collectors.toSet())); - - public static final Set ROLLERSHUTTER_SLATS_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(THING_TYPE_ROLLERSHUTTER_SLATS).collect(Collectors.toSet())); - - public static final Set BISTABIEL_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(THING_TYPE_ON_OFF_LIGHT, THING_TYPE_TIMER_LIGHT).collect(Collectors.toSet())); - - public static final Set THERMOSTAT_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(THING_TYPE_THERMOSTAT).collect(Collectors.toSet())); - - public static final Set DIMMER_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(THING_TYPE_ON_OFF_LIGHT, THING_TYPE_DIMMABLE_LIGHT).collect(Collectors.toSet())); - - public static final Set SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(Stream - .of(THING_TYPE_TIMER_LIGHT, THING_TYPE_ON_OFF_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_THERMOSTAT, - THING_TYPE_SCENE, THING_TYPE_CO2, THING_TYPE_ROLLERSHUTTER, THING_TYPE_ROLLERSHUTTER_SLATS) - .collect(Collectors.toSet())); - - // List of all Channel ids - public static final String CHANNEL_SWITCH = "switch"; - public static final String CHANNEL_BRIGHTNESS = "brightness"; - public static final String CHANNEL_MEASURED = "measured"; - public static final String CHANNEL_SETPOINT = "setpoint"; - public static final String CHANNEL_MODE = "mode"; - public static final String CHANNEL_CO2 = "co2"; - public static final String CHANNEL_ROLLERSHUTTER = "rollershutter"; - public static final String CHANNEL_SLATS = "slats"; - - // Thing config properties - public static final String CONFIG_BISTABIEL_ID = "bistabielId"; - public static final String CONFIG_DIMMER_ID = "dimmerId"; - public static final String CONFIG_THERMOSTAT_ID = "thermostatId"; - public static final String CONFIG_SCENE_ID = "sceneId"; - public static final String CONFIG_CO2_ID = "co2Id"; - public static final String CONFIG_ROLLERSHUTTER_ID = "rolId"; - public static final String CONFIG_STEP_VALUE = "step"; -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java deleted file mode 100644 index 4377a1fd5803f..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java +++ /dev/null @@ -1,241 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ - -package org.openhab.binding.qbus.internal; - -import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; - -import java.net.InetAddress; -import java.net.UnknownHostException; -import java.util.Map; -import java.util.Map.Entry; -import java.util.concurrent.ScheduledFuture; -import java.util.concurrent.TimeUnit; - -import org.openhab.binding.qbus.internal.protocol.QbusCommunication; -import org.openhab.core.config.core.Configuration; -import org.openhab.core.thing.Bridge; -import org.openhab.core.thing.ChannelUID; -import org.openhab.core.thing.ThingStatus; -import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.thing.binding.BaseBridgeHandler; -import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * {@link QbusBridgeHandler} is the handler for a Qbus controller - * - * @author Koen Schockaert - Initial Contribution - */ -public class QbusBridgeHandler extends BaseBridgeHandler { - - private final Logger logger = LoggerFactory.getLogger(QbusBridgeHandler.class); - - private QbusCommunication qbusComm; - - private ScheduledFuture refreshTimer; - - public QbusBridgeHandler(Bridge Bridge) { - super(Bridge); - } - - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - // There is nothing to handle in the bridge handler - } - - @Override - public void initialize() { - logger.debug("QBUS: initializing bridge handler"); - - Configuration config = this.getConfig(); - InetAddress addr = getAddr(); - int port = getPort(); - - logger.debug("Qbus: bridge handler host {}, port {}", addr, port); - - if (addr != null) { - createCommunicationObject(addr, port); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - "Qbus: cannot resolve bridge IP with hostname " + config.get(CONFIG_HOST_NAME)); - } - } - - /** - * Create communication object to Qbus server and start communication. - * - * @param addr : IP address of Qbus server - * @param port : Communication port of QbusServer - * @param sn : Serial number of Controller - */ - private void createCommunicationObject(InetAddress addr, int port) { - Configuration config = this.getConfig(); - scheduler.submit(() -> { - qbusComm = new QbusCommunication(); - - // Set callback from Qbus object to this bridge to be able to take bridge - // offline when non-resolvable communication error occurs. - setBridgeCallBack(); - - qbusComm.startCommunication(); - if (!qbusComm.communicationActive()) { - qbusComm = null; - bridgeOffline(); - return; - } - - updateStatus(ThingStatus.ONLINE); - - Integer refreshInterval = ((Number) config.get(CONFIG_REFRESH)).intValue(); - setupRefreshTimer(refreshInterval); - - }); - } - - private void setBridgeCallBack() { - this.qbusComm.setBridgeCallBack(this); - } - - /** - * Schedule future communication refresh. - * - * @param interval_config Time before refresh in minutes. - */ - private void setupRefreshTimer(Integer refreshInterval) { - if (this.refreshTimer != null) { - this.refreshTimer.cancel(true); - this.refreshTimer = null; - } - - if ((refreshInterval == null) || (refreshInterval == 0)) { - return; - } - - // This timer will restart the bridge connection periodically - logger.debug("Qbus: Checking for Client communication every {} min", refreshInterval); - this.refreshTimer = scheduler.scheduleWithFixedDelay(() -> { - logger.debug("Qbus: check communication after timerinterval"); - - if (!qbusComm.communicationActive()) { - logger.debug("Qbus: Restarting communication"); - qbusComm.restartCommunication(); - - if (!qbusComm.communicationActive()) { - qbusComm = null; - bridgeOffline(); - updateStatus(ThingStatus.OFFLINE); - return; - } - - // updateStatus(ThingStatus.ONLINE); - updateStatus(ThingStatus.ONLINE); - } else { - logger.debug("Qbus: Communication still active"); - } - - }, refreshInterval, refreshInterval, TimeUnit.MINUTES); - } - - /** - * Take bridge offline when error in communication with Qbus server. This method can also be - * called directly from {@link QbusCommunication} object. - */ - public void bridgeOffline() { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - "Qbus: error starting bridge connection"); - } - - /** - * Put bridge online when error in communication resolved. - */ - public void bridgeOnline() { - updateStatus(ThingStatus.ONLINE); - } - - @Override - public boolean isInitialized() { - return true; - } - - @Override - public void dispose() { - if (this.refreshTimer != null) { - this.refreshTimer.cancel(true); - } - this.refreshTimer = null; - } - - @Override - public void handleConfigurationUpdate(Map configurationParameters) { - Configuration configuration = editConfiguration(); - for (Entry configurationParmeter : configurationParameters.entrySet()) { - configuration.put(configurationParmeter.getKey(), configurationParmeter.getValue()); - } - updateConfiguration(configuration); - - scheduler.submit(() -> { - - updateStatus(ThingStatus.ONLINE); - - Integer refreshInterval = ((Number) configuration.get(CONFIG_REFRESH)).intValue(); - setupRefreshTimer(refreshInterval); - }); - } - - /** - * Get the Qbus communication object. - * - * @return Qbus communication object - */ - public QbusCommunication getCommunication() { - return this.qbusComm; - } - - /** - * Get the IP-address of the Qbus server. - * - * @return the addr - */ - public InetAddress getAddr() { - Configuration config = this.getConfig(); - InetAddress addr = null; - try { - addr = InetAddress.getByName((String) config.get(CONFIG_HOST_NAME)); - } catch (UnknownHostException e) { - logger.debug("Qbus: Cannot resolve hostname {} to IP adress", config.get(CONFIG_HOST_NAME)); - } - return addr; - } - - /** - * Get the listening port of the Qbus server. - * - * @return the port - */ - public int getPort() { - Configuration config = this.getConfig(); - return ((Number) config.get(CONFIG_PORT)).intValue(); - } - - /** - * Get the serial nr of the Qbus server. - * - * @return the sn - */ - public String getSn() { - Configuration config = this.getConfig(); - return ((String) config.get(CONFIG_SN)); - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java deleted file mode 100644 index 5a1cb996c4a91..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal; - -import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; - -import org.openhab.binding.qbus.internal.handler.QbusBistabielHandler; -import org.openhab.binding.qbus.internal.handler.QbusCO2Handler; -import org.openhab.binding.qbus.internal.handler.QbusDimmerHandler; -import org.openhab.binding.qbus.internal.handler.QbusRolHandler; -import org.openhab.binding.qbus.internal.handler.QbusSceneHandler; -import org.openhab.binding.qbus.internal.handler.QbusThermostatHandler; -import org.openhab.core.thing.Bridge; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingTypeUID; -import org.openhab.core.thing.binding.BaseThingHandlerFactory; -import org.openhab.core.thing.binding.ThingHandler; -import org.openhab.core.thing.binding.ThingHandlerFactory; -import org.osgi.service.component.annotations.Component; - -/** - * The {@link qbusHandlerFactory} is responsible for creating things and thing - * handlers. - * - * @author Koen Schockaert - Initial Contribution - */ - -@Component(service = ThingHandlerFactory.class, configurationPid = "binding.qbus") -public class QbusHandlerFactory extends BaseThingHandlerFactory { - - @Override - public boolean supportsThingType(ThingTypeUID thingTypeUID) { - return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID) || BRIDGE_THING_TYPES_UIDS.contains(thingTypeUID); - } - - @Override - protected ThingHandler createHandler(Thing thing) { - if (BRIDGE_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { - QbusBridgeHandler handler = new QbusBridgeHandler((Bridge) thing); - return handler; - } else if (SCENE_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { - return new QbusSceneHandler(thing); - } else if (BISTABIEL_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { - return new QbusBistabielHandler(thing); - } else if (THERMOSTAT_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { - return new QbusThermostatHandler(thing); - } else if (DIMMER_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { - return new QbusDimmerHandler(thing); - } else if (CO2_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { - return new QbusCO2Handler(thing); - } else if (ROLLERSHUTTER_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { - return new QbusRolHandler(thing); - } else if (ROLLERSHUTTER_SLATS_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { - return new QbusRolHandler(thing); - } - - return null; - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java deleted file mode 100644 index fb45b86151077..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java +++ /dev/null @@ -1,183 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.handler; - -import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; -import static org.openhab.core.types.RefreshType.REFRESH; - -import java.util.HashMap; -import java.util.Map; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.qbus.internal.QbusBridgeHandler; -import org.openhab.binding.qbus.internal.protocol.QbusBistabiel; -import org.openhab.binding.qbus.internal.protocol.QbusCommunication; -import org.openhab.core.config.core.Configuration; -import org.openhab.core.library.types.OnOffType; -import org.openhab.core.thing.Bridge; -import org.openhab.core.thing.ChannelUID; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingStatus; -import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.thing.binding.BaseThingHandler; -import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link QbusBistabielHandler} is responsible for handling commands, which are - * sent to one of the channels. - * - * @author Koen Schockaert - Initial Contribution - */ -@NonNullByDefault -public class QbusBistabielHandler extends BaseThingHandler { - - private final Logger logger = LoggerFactory.getLogger(QbusBistabielHandler.class); - - // private volatile int prevBistabielState; - - public QbusBistabielHandler(Thing thing) { - super(thing); - } - - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - Integer BistabielId = ((Number) this.getConfig().get(CONFIG_BISTABIEL_ID)).intValue(); - - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute bistabiel " + BistabielId); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute bistabiel " + BistabielId); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - - if (QComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: bridge communication not initialized when trying to execute bistabiel " + BistabielId); - return; - } - - QbusBistabiel QBistabiel = QComm.getBistabiel().get(BistabielId); - - if (QComm.communicationActive()) { - handleCommandSelection(QBistabiel, channelUID, command); - } else { - // We lost connection but the connection object is there, so was correctly started. - // Try to restart communication. - // This can be expensive, therefore do it in a job. - scheduler.submit(() -> { - QComm.restartCommunication(); - // If still not active, take thing offline and return. - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - // Also put the bridge back online - QBridgeHandler.bridgeOnline(); - - // And finally handle the command - handleCommandSelection(QBistabiel, channelUID, command); - }); - } - } - - private void handleCommandSelection(QbusBistabiel QBistabiel, ChannelUID channelUID, Command command) { - logger.debug("Qbus: handle command {} for {}", command, channelUID); - - if (command == REFRESH) { - handleStateUpdate(QBistabiel); - return; - } - - handleSwitchCommand(QBistabiel, command); - updateStatus(ThingStatus.ONLINE); - } - - private void handleSwitchCommand(QbusBistabiel QBistabiel, Command command) { - if (command instanceof OnOffType) { - OnOffType s = (OnOffType) command; - - @SuppressWarnings("null") - String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); - - if (s == OnOffType.OFF) { - QBistabiel.execute(0, sn); - } else { - QBistabiel.execute(100, sn); - } - } - } - - @Override - public void initialize() { - Configuration config = this.getConfig(); - - Integer BistabielId = ((Number) config.get(CONFIG_BISTABIEL_ID)).intValue(); - - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for bistabiel " + BistabielId); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for bistabiel " + BistabielId); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null || !QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: no connection with Qbus server, could not initialize bistabiel " + BistabielId); - return; - } - - QbusBistabiel QBistabiel = QComm.getBistabiel().get(BistabielId); - - // int bistabielState = QBistabiel.getState(); - - // this.prevBistabielState = bistabielState; - QBistabiel.setThingHandler(this); - - Map properties = new HashMap<>(); - - thing.setProperties(properties); - - handleStateUpdate(QBistabiel); - - logger.debug("Qbus: bistabiel intialized {}", BistabielId); - } - - /** - * Method to update state of channel, called from Qbus Bistabiel. - */ - public void handleStateUpdate(QbusBistabiel QBistabiel) { - - int bistabielState = QBistabiel.getState(); - - updateState(CHANNEL_SWITCH, (bistabielState == 0) ? OnOffType.OFF : OnOffType.ON); - updateStatus(ThingStatus.ONLINE); - - // this.prevBistabielState = bistabielState; - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java deleted file mode 100644 index b500901e62073..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java +++ /dev/null @@ -1,167 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.handler; - -import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; -import static org.openhab.core.types.RefreshType.REFRESH; - -import java.util.HashMap; -import java.util.Map; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.qbus.internal.QbusBridgeHandler; -import org.openhab.binding.qbus.internal.protocol.QbusCO2; -import org.openhab.binding.qbus.internal.protocol.QbusCommunication; -import org.openhab.core.config.core.Configuration; -import org.openhab.core.library.types.DecimalType; -import org.openhab.core.thing.Bridge; -import org.openhab.core.thing.ChannelUID; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingStatus; -import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.thing.binding.BaseThingHandler; -import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link QbusCO2Handler} is responsible for handling commands, which are - * sent to one of the channels. - * - * @author Koen Schockaert - Initial Contribution - */ -@NonNullByDefault -public class QbusCO2Handler extends BaseThingHandler { - - private final Logger logger = LoggerFactory.getLogger(QbusCO2Handler.class); - - // private volatile int prevCO2State; - - public QbusCO2Handler(Thing thing) { - super(thing); - } - - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - Integer CO2Id = ((Number) this.getConfig().get(CONFIG_CO2_ID)).intValue(); - - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute CO2 " + CO2Id); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute CO2 " + CO2Id); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - - if (QComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: bridge communication not initialized when trying to execute CO2 " + CO2Id); - return; - } - - QbusCO2 QCO2 = QComm.getCo2().get(CO2Id); - - if (QComm.communicationActive()) { - handleCommandSelection(QCO2, channelUID, command); - } else { - // We lost connection but the connection object is there, so was correctly started. - // Try to restart communication. - // This can be expensive, therefore do it in a job. - scheduler.submit(() -> { - QComm.restartCommunication(); - // If still not active, take thing offline and return. - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - // Also put the bridge back online - QBridgeHandler.bridgeOnline(); - - // And finally handle the command - handleCommandSelection(QCO2, channelUID, command); - }); - } - } - - private void handleCommandSelection(QbusCO2 QCO2, ChannelUID channelUID, Command command) { - logger.debug("Qbus: handle command {} for {}", command, channelUID); - - if (command == REFRESH) { - handleStateUpdate(QCO2); - return; - } - - updateStatus(ThingStatus.ONLINE); - } - - @Override - public void initialize() { - Configuration config = this.getConfig(); - - Integer CO2Id = ((Number) config.get(CONFIG_CO2_ID)).intValue(); - - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for CO2 " + CO2Id); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for CO2 " + CO2Id); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null || !QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: no connection with Qbus server, could not initialize CO2 " + CO2Id); - return; - } - - QbusCO2 QCO2 = QComm.getCo2().get(CO2Id); - - // int actionState = QCO2.getState(); - - // this.prevCO2State = actionState; - QCO2.setThingHandler(this); - - Map properties = new HashMap<>(); - - thing.setProperties(properties); - - handleStateUpdate(QCO2); - - logger.debug("Qbus: CO2 intialized {}", CO2Id); - } - - /** - * Method to update state of channel, called from Qbus CO2. - */ - public void handleStateUpdate(QbusCO2 QCO2) { - - int CO2State = QCO2.getState(); - - updateState(CHANNEL_CO2, new DecimalType(CO2State)); - updateStatus(ThingStatus.ONLINE); - - // this.prevCO2State = CO2State; - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java deleted file mode 100644 index 0ff3d2412fa00..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java +++ /dev/null @@ -1,250 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.handler; - -import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; -import static org.openhab.core.types.RefreshType.REFRESH; - -import java.util.HashMap; -import java.util.Map; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.qbus.internal.QbusBridgeHandler; -import org.openhab.binding.qbus.internal.protocol.QbusCommunication; -import org.openhab.binding.qbus.internal.protocol.QbusDimmer; -import org.openhab.core.config.core.Configuration; -import org.openhab.core.library.types.IncreaseDecreaseType; -import org.openhab.core.library.types.OnOffType; -import org.openhab.core.library.types.PercentType; -import org.openhab.core.thing.Bridge; -import org.openhab.core.thing.ChannelUID; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingStatus; -import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.thing.binding.BaseThingHandler; -import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link QbusDimmerHandler} is responsible for handling commands, which are - * sent to one of the channels. - * - * @author Koen Schockaert - Initial Contribution - */ -@NonNullByDefault -public class QbusDimmerHandler extends BaseThingHandler { - - private final Logger logger = LoggerFactory.getLogger(QbusDimmerHandler.class); - - // private volatile int prevDimmerState; - - public QbusDimmerHandler(Thing thing) { - super(thing); - } - - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - Integer dimmerId = ((Number) this.getConfig().get(CONFIG_DIMMER_ID)).intValue(); - - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute dimmer " + dimmerId); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute dimmer " + dimmerId); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - - if (QComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: bridge communication not initialized when trying to execute dimmer " + dimmerId); - return; - } - - QbusDimmer QDimmer = QComm.getDimmer().get(dimmerId); - - /* - * if (QDimmer == null) { - * updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - * "Qbus: dimmerId " + dimmerId + " does not match a dimmer in the controller"); - * return; - * } - */ - if (QComm.communicationActive()) { - handleCommandSelection(QDimmer, channelUID, command); - } else { - // We lost connection but the connection object is there, so was correctly started. - // Try to restart communication. - // This can be expensive, therefore do it in a job. - scheduler.submit(() -> { - QComm.restartCommunication(); - // If still not active, take thing offline and return. - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - // Also put the bridge back online - QBridgeHandler.bridgeOnline(); - - // And finally handle the command - handleCommandSelection(QDimmer, channelUID, command); - }); - } - } - - private void handleCommandSelection(QbusDimmer QDimmer, ChannelUID channelUID, Command command) { - logger.debug("Qbus: handle command {} for {}", command, channelUID); - - if (command == REFRESH) { - handleStateUpdate(QDimmer); - return; - } - - switch (channelUID.getId()) { - case CHANNEL_SWITCH: - handleSwitchCommand(QDimmer, command); - updateStatus(ThingStatus.ONLINE); - break; - - case CHANNEL_BRIGHTNESS: - handleBrightnessCommand(QDimmer, command); - updateStatus(ThingStatus.ONLINE); - break; - - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: channel unknown " + channelUID.getId()); - } - } - - private void handleSwitchCommand(QbusDimmer QDimmer, Command command) { - - @SuppressWarnings("null") - String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); - - if (command instanceof OnOffType) { - OnOffType s = (OnOffType) command; - if (s == OnOffType.OFF) { - QDimmer.execute(0, sn); - } else { - QDimmer.execute(100, sn); - } - } - } - - private void handleBrightnessCommand(QbusDimmer QDimmer, Command command) { - // Bridge QBridge = getBridge(); - @SuppressWarnings("null") - String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); - - if (command instanceof OnOffType) { - OnOffType s = (OnOffType) command; - if (s == OnOffType.OFF) { - QDimmer.execute(0, sn); - } else { - QDimmer.execute(100, sn); - } - } else if (command instanceof IncreaseDecreaseType) { - IncreaseDecreaseType s = (IncreaseDecreaseType) command; - int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); - int currentValue = QDimmer.getState(); - int newValue; - if (s == IncreaseDecreaseType.INCREASE) { - newValue = currentValue + stepValue; - // round down to step multiple - newValue = newValue - newValue % stepValue; - QDimmer.execute(newValue > 100 ? 100 : newValue, sn); - } else { - newValue = currentValue - stepValue; - // round up to step multiple - newValue = newValue + newValue % stepValue; - QDimmer.execute(newValue < 0 ? 0 : newValue, sn); - } - } else if (command instanceof PercentType) { - PercentType p = (PercentType) command; - if (p == PercentType.ZERO) { - QDimmer.execute(0, sn); - } else { - QDimmer.execute(p.intValue(), sn); - } - } - } - - @Override - public void initialize() { - Configuration config = this.getConfig(); - - Integer dimmerId = ((Number) config.get(CONFIG_DIMMER_ID)).intValue(); - - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for dimmer " + dimmerId); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for dimmer " + dimmerId); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null || !QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: no connection with Qbus, could not initialize dimmer " + dimmerId); - return; - } - - QbusDimmer QDimmer = QComm.getDimmer().get(dimmerId); - /* - * if (QDimmer == null) { - * updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - * "Qbus: dimmerId does not match an action in the server " + dimmerId); - * return; - * } - */ - // int actionState = QDimmer.getState(); - - // this.prevDimmerState = actionState; - QDimmer.setThingHandler(this); - - Map properties = new HashMap<>(); - - thing.setProperties(properties); - - handleStateUpdate(QDimmer); - - logger.debug("Qbus: dimmer intialized {}", dimmerId); - } - - /** - * Method to update state of channel, called from Qbus Dimmer. - */ - public void handleStateUpdate(QbusDimmer QDimmer) { - - int dimmerState = QDimmer.getState(); - - updateState(CHANNEL_BRIGHTNESS, new PercentType(dimmerState)); - - updateStatus(ThingStatus.ONLINE); - - // this.prevDimmerState = dimmerState; - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java deleted file mode 100644 index fa9bad35e3458..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java +++ /dev/null @@ -1,264 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.handler; - -import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; -import static org.openhab.core.types.RefreshType.REFRESH; - -import java.util.HashMap; -import java.util.Map; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.qbus.internal.QbusBridgeHandler; -import org.openhab.binding.qbus.internal.protocol.QbusCommunication; -import org.openhab.binding.qbus.internal.protocol.QbusRol; -import org.openhab.core.config.core.Configuration; -import org.openhab.core.library.types.IncreaseDecreaseType; -import org.openhab.core.library.types.PercentType; -import org.openhab.core.thing.Bridge; -import org.openhab.core.thing.ChannelUID; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingStatus; -import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.thing.binding.BaseThingHandler; -import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link QbusRolHandler} is responsible for handling commands, which are - * sent to one of the channels. - * - * @author Koen Schockaert - Initial Contribution - */ -@NonNullByDefault -public class QbusRolHandler extends BaseThingHandler { - - private final Logger logger = LoggerFactory.getLogger(QbusRolHandler.class); - - // private volatile int prevRolState; - // private volatile int prevSlatState; - - public QbusRolHandler(Thing thing) { - super(thing); - } - - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - Integer RolId = ((Number) this.getConfig().get(CONFIG_ROLLERSHUTTER_ID)).intValue(); - - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute Slats " + RolId); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute Slats " + RolId); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - - if (QComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: bridge communication not initialized when trying to execute Slats " + RolId); - return; - } - - QbusRol QRol = QComm.getRol().get(RolId); - - if (QComm.communicationActive()) { - handleCommandSelection(QRol, channelUID, command); - } else { - // We lost connection but the connection object is there, so was correctly started. - // Try to restart communication. - // This can be expensive, therefore do it in a job. - scheduler.submit(() -> { - QComm.restartCommunication(); - // If still not active, take thing offline and return. - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - // Also put the bridge back online - QBridgeHandler.bridgeOnline(); - - // And finally handle the command - handleCommandSelection(QRol, channelUID, command); - }); - } - } - - private void handleCommandSelection(QbusRol qRol, ChannelUID channelUID, Command command) { - logger.debug("Qbus: handle command {} for {}", command, channelUID); - - if (command == REFRESH) { - handleStateUpdate(qRol); - return; - } - - switch (channelUID.getId()) { - case CHANNEL_ROLLERSHUTTER: - handleBrightnessCommand(qRol, command); - updateStatus(ThingStatus.ONLINE); - break; - - case CHANNEL_SLATS: - handleSlatsCommand(qRol, command); - updateStatus(ThingStatus.ONLINE); - break; - - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: channel unknown " + channelUID.getId()); - } - } - - private void handleBrightnessCommand(QbusRol QRol, Command command) { - @SuppressWarnings("null") - String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); - - if (command instanceof org.openhab.core.library.types.UpDownType) { - org.openhab.core.library.types.UpDownType s = (org.openhab.core.library.types.UpDownType) command; - if (s == org.openhab.core.library.types.UpDownType.DOWN) { - QRol.execute(0, sn); - } else { - QRol.execute(100, sn); - } - } else if (command instanceof IncreaseDecreaseType) { - IncreaseDecreaseType s = (IncreaseDecreaseType) command; - int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); - int currentValue = QRol.getState(); - int newValue; - if (s == IncreaseDecreaseType.INCREASE) { - newValue = currentValue + stepValue; - // round down to step multiple - newValue = newValue - newValue % stepValue; - QRol.execute(newValue > 100 ? 100 : newValue, sn); - } else { - newValue = currentValue - stepValue; - // round up to step multiple - newValue = newValue + newValue % stepValue; - QRol.execute(newValue < 0 ? 0 : newValue, sn); - } - } else if (command instanceof PercentType) { - PercentType p = (PercentType) command; - if (p == PercentType.ZERO) { - QRol.execute(0, sn); - } else { - QRol.execute(p.intValue(), sn); - } - } - } - - private void handleSlatsCommand(QbusRol QRol, Command command) { - @SuppressWarnings("null") - String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); - - if (command instanceof org.openhab.core.library.types.UpDownType) { - org.openhab.core.library.types.UpDownType s = (org.openhab.core.library.types.UpDownType) command; - if (s == org.openhab.core.library.types.UpDownType.DOWN) { - QRol.executeSlats(0, sn); - } else { - QRol.executeSlats(100, sn); - } - } else if (command instanceof IncreaseDecreaseType) { - IncreaseDecreaseType s = (IncreaseDecreaseType) command; - int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); - int currentValue = QRol.getState(); - int newValue; - if (s == IncreaseDecreaseType.INCREASE) { - newValue = currentValue + stepValue; - // round down to step multiple - newValue = newValue - newValue % stepValue; - QRol.executeSlats(newValue > 100 ? 100 : newValue, sn); - } else { - newValue = currentValue - stepValue; - // round up to step multiple - newValue = newValue + newValue % stepValue; - QRol.executeSlats(newValue < 0 ? 0 : newValue, sn); - } - } else if (command instanceof PercentType) { - PercentType p = (PercentType) command; - if (p == PercentType.ZERO) { - QRol.executeSlats(0, sn); - } else { - QRol.executeSlats(p.intValue(), sn); - } - } - } - - @Override - public void initialize() { - Configuration config = this.getConfig(); - - Integer RolId = ((Number) config.get(CONFIG_ROLLERSHUTTER_ID)).intValue(); - - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for Slats " + RolId); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for Slats " + RolId); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null || !QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: no connection with Qbus server, could not initialize Slats " + RolId); - return; - } - - QbusRol QRol = QComm.getRol().get(RolId); - /* - * int rolState = QRol.getState(); - * int slatState = QRol.getStateSlats(); - * - * this.prevRolState = rolState; - * this.prevSlatState = slatState; - * - */ - QRol.setThingHandler(this); - - Map properties = new HashMap<>(); - - thing.setProperties(properties); - - handleStateUpdate(QRol); - - logger.debug("Qbus: Slats intialized {}", RolId); - } - - /** - * Method to update state of channel, called from Qbus Slats. - */ - public void handleStateUpdate(QbusRol qRol) { - - int rolState = qRol.getState().intValue(); - int slatState = qRol.getStateSlats().intValue(); - - updateState(CHANNEL_ROLLERSHUTTER, new PercentType(rolState)); - updateState(CHANNEL_SLATS, new PercentType(slatState)); - updateStatus(ThingStatus.ONLINE); - - // this.prevRolState = rolState; - // this.prevSlatState = slatState; - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java deleted file mode 100644 index ca810f3bd4fe7..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java +++ /dev/null @@ -1,181 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.handler; - -import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; -import static org.openhab.core.types.RefreshType.REFRESH; - -import java.util.HashMap; -import java.util.Map; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.qbus.internal.QbusBridgeHandler; -import org.openhab.binding.qbus.internal.protocol.QbusCommunication; -import org.openhab.binding.qbus.internal.protocol.QbusScene; -import org.openhab.core.config.core.Configuration; -import org.openhab.core.library.types.OnOffType; -import org.openhab.core.thing.Bridge; -import org.openhab.core.thing.ChannelUID; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingStatus; -import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.thing.binding.BaseThingHandler; -import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link QbusSceneHandler} is responsible for handling commands, which are - * sent to one of the channels. - * - * @author Koen Schockaert - Initial Contribution - */ -@NonNullByDefault -public class QbusSceneHandler extends BaseThingHandler { - - private final Logger logger = LoggerFactory.getLogger(QbusSceneHandler.class); - - // private volatile int prevSceneState; - - public QbusSceneHandler(Thing thing) { - super(thing); - } - - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - Integer SceneId = ((Number) this.getConfig().get(CONFIG_SCENE_ID)).intValue(); - - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute Scene " + SceneId); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute Scene " + SceneId); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - - if (QComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: bridge communication not initialized when trying to execute scene " + SceneId); - return; - } - - QbusScene QScene = QComm.getScenes().get(SceneId); - - if (QComm.communicationActive()) { - handleCommandSelection(QScene, channelUID, command); - } else { - // We lost connection but the connection object is there, so was correctly started. - // Try to restart communication. - // This can be expensive, therefore do it in a job. - scheduler.submit(() -> { - QComm.restartCommunication(); - // If still not active, take thing offline and return. - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - // Also put the bridge back online - QBridgeHandler.bridgeOnline(); - - // And finally handle the command - handleCommandSelection(QScene, channelUID, command); - }); - } - } - - private void handleCommandSelection(QbusScene QScene, ChannelUID channelUID, Command command) { - logger.debug("Qbus: handle command {} for {}", command, channelUID); - - if (command == REFRESH) { - handleStateUpdate(QScene); - return; - } - - handleSwitchCommand(QScene, command); - updateStatus(ThingStatus.ONLINE); - } - - private void handleSwitchCommand(QbusScene QScene, Command command) { - @SuppressWarnings("null") - String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); - if (command instanceof OnOffType) { - OnOffType s = (OnOffType) command; - if (s == OnOffType.OFF) { - QScene.execute(0, sn); - } else { - QScene.execute(100, sn); - } - } - } - - @Override - public void initialize() { - Configuration config = this.getConfig(); - - Integer SceneId = ((Number) config.get(CONFIG_SCENE_ID)).intValue(); - - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for scene " + SceneId); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for scene " + SceneId); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null || !QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: no connection with Qbus server, could not initialize scene " + SceneId); - return; - } - - QbusScene QScene = QComm.getScenes().get(SceneId); - - // int sceneState = QScene.getState(); - /* - * this.prevSceneState = sceneState; - * QScene.setThingHandler(this); - */ - Map properties = new HashMap<>(); - - thing.setProperties(properties); - - handleStateUpdate(QScene); - - logger.debug("Qbus: scene intialized {}", SceneId); - } - - /** - * Method to update state of channel, called from Qbus Scene. - */ - public void handleStateUpdate(QbusScene QScene) { - - int sceneState = QScene.getState(); - - updateState(CHANNEL_SWITCH, (sceneState == 0) ? OnOffType.OFF : OnOffType.ON); - updateStatus(ThingStatus.ONLINE); - - // this.prevSceneState = sceneState; - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java deleted file mode 100644 index 3120718e73e01..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java +++ /dev/null @@ -1,183 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.handler; - -import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; -import static org.openhab.core.library.unit.SIUnits.CELSIUS; -import static org.openhab.core.types.RefreshType.REFRESH; - -import javax.measure.quantity.Temperature; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.openhab.binding.qbus.internal.QbusBridgeHandler; -import org.openhab.binding.qbus.internal.protocol.QThermostat; -import org.openhab.binding.qbus.internal.protocol.QbusCommunication; -import org.openhab.core.config.core.Configuration; -import org.openhab.core.library.types.DecimalType; -import org.openhab.core.library.types.QuantityType; -import org.openhab.core.thing.Bridge; -import org.openhab.core.thing.ChannelUID; -import org.openhab.core.thing.Thing; -import org.openhab.core.thing.ThingStatus; -import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.thing.binding.BaseThingHandler; -import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link QbusThermostatHandler} is responsible for handling commands, which are - * sent to one of the channels. - * - * @author Koen Schockaert - Initial Contribution - */ -@NonNullByDefault -public class QbusThermostatHandler extends BaseThingHandler { - - private final Logger logger = LoggerFactory.getLogger(QbusThermostatHandler.class); - - public QbusThermostatHandler(Thing thing) { - super(thing); - } - - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - Integer thermostatId = ((Number) this.getConfig().get(CONFIG_THERMOSTAT_ID)).intValue(); - - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute thermostat command " + thermostatId); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute thermostat command " + thermostatId); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - - if (QComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: bridge communication not initialized when trying to execute thermostat command " - + thermostatId); - return; - } - - QThermostat qThermostat = QComm.getThermostats().get(thermostatId); - - if (QComm.communicationActive()) { - handleCommandSelection(qThermostat, channelUID, command); - } else { - scheduler.submit(() -> { - QComm.restartCommunication(); - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - QBridgeHandler.bridgeOnline(); - handleCommandSelection(qThermostat, channelUID, command); - }); - } - } - - @SuppressWarnings("unchecked") - private void handleCommandSelection(QThermostat qThermostat, ChannelUID channelUID, Command command) { - logger.debug("Qbus: handle command {} for {}", command, channelUID); - @SuppressWarnings("null") - String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); - - if (REFRESH.equals(command)) { - handleStateUpdate(qThermostat); - return; - } - - switch (channelUID.getId()) { - case CHANNEL_MEASURED: - updateStatus(ThingStatus.ONLINE); - break; - - case CHANNEL_MODE: - if (command instanceof DecimalType) { - qThermostat.executeMode(((DecimalType) command).intValue(), sn); - } - updateStatus(ThingStatus.ONLINE); - break; - - case CHANNEL_SETPOINT: - if (command instanceof QuantityType) { - qThermostat.executeSetpoint(((QuantityType) command).doubleValue(), sn); - } - updateStatus(ThingStatus.ONLINE); - - break; - - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: channel unknown " + channelUID.getId()); - } - } - - @Override - public void initialize() { - Configuration config = this.getConfig(); - - Integer thermostatId = ((Number) config.get(CONFIG_THERMOSTAT_ID)).intValue(); - - Bridge nhcBridge = getBridge(); - if (nhcBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for thermostat " + thermostatId); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) nhcBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for thermostat " + thermostatId); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null || !QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: no connection with Qbus server, could not initialize thermostat " + thermostatId); - return; - } - - QThermostat qThermostat = QComm.getThermostats().get(thermostatId); - - qThermostat.setThingHandler(this); - - handleStateUpdate(qThermostat); - - logger.debug("Qbus: thermostat intialized {}", thermostatId); - } - - /** - * Method to update state of all channels, called from Qbus thermostat. - * - * @param qThermostat Qbus thermostat - * - */ - public void handleStateUpdate(QThermostat qThermostat) { - - updateState(CHANNEL_MEASURED, new QuantityType(qThermostat.getMeasured(), CELSIUS)); - - updateState(CHANNEL_SETPOINT, new QuantityType(qThermostat.getSetpoint(), CELSIUS)); - - updateState(CHANNEL_MODE, new DecimalType(qThermostat.getMode())); - - updateStatus(ThingStatus.ONLINE); - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java deleted file mode 100644 index 78b89d7543ef5..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.protocol; - -/** - * Class {@link QbusMessageCmd} used as input to gson to send commands to Qbus. Extends - * {@link QbusMessageBase}. - *

- * Example: {"cmd":"executebistabiel","id":1,"value1":0} - * - * @author Koen Schockaert - Initial Contribution - */ - -@SuppressWarnings("unused") -class QMessageCmd extends QbusMessageBase { - - private int id; - private Integer pos; - private Integer value1; - private Integer value2; - private Integer value3; - private Integer mode; - private Double setpoint; - private String ctdsn; - - QMessageCmd(String cmd) { - super.setCmd(cmd); - } - - QMessageCmd(String cmd, int id) { - this(cmd); - this.id = id; - } - - QMessageCmd(String cmd, int id, Integer value1) { - this(cmd, id); - this.value1 = value1; - } - - QMessageCmd(String cmd, int id, Integer value1, Integer value2) { - this(cmd, id, value1); - this.value2 = value2; - } - - QMessageCmd(String cmd, int id, Integer value1, Integer value2, Integer value3) { - this(cmd, id, value1); - this.value2 = value2; - this.value3 = value3; - } - - QMessageCmd withMode(Integer mode) { - this.mode = mode; - return this; - } - - QMessageCmd withSetpoint(Double d) { - this.setpoint = d; - return this; - } - - QMessageCmd withSn(String sn) { - this.ctdsn = sn; - return this; - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageListMap.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageListMap.java deleted file mode 100644 index 1bc73610537a0..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageListMap.java +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.protocol; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; - -/** - * Class {@link QbusMessageListMap} used as output from gson for cmd or event feedback from Qbus where the - * data part is enclosed by [] and contains a list of json strings. Extends {@link QbusMessageBase}. - *

- * - * @author Koen Schockaert - Initial Contribution - */ - -class QMessageListMap extends QbusMessageBase { - - private List> data = new ArrayList<>(); - - List> getData() { - return this.data; - } - - void setData(List> data) { - this.data = data; - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageMap.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageMap.java deleted file mode 100644 index 139ecee65bd47..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageMap.java +++ /dev/null @@ -1,36 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.protocol; - -import java.util.HashMap; -import java.util.Map; - -/** - * Class {@link QbusMessageMap} used as output from gson for cmd or event feedback from Qbus where the - * data part is a simple json string. Extends {@link QbusMessageBase}. - *

- * - * @author Koen Schockaert - Initial Contribution - */ -class QMessageMap extends QbusMessageBase { - - private Map data = new HashMap<>(); - - Map getData() { - return this.data; - } - - void setData(Map data) { - this.data = data; - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QThermostat.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QThermostat.java deleted file mode 100644 index 4663194c57fa8..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QThermostat.java +++ /dev/null @@ -1,175 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.protocol; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.qbus.internal.handler.QbusThermostatHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link QThermostat} class represents the thermostat Qbus communication object. It contains all - * fields representing a Qbus thermostat and has methods to set the thermostat mode and setpoint in Qbus and - * receive thermostat updates. - * - * @author Koen Schockaert - Initial Contribution - */ -@NonNullByDefault -public final class QThermostat { - - private final Logger logger = LoggerFactory.getLogger(QThermostat.class); - - @Nullable - private QbusCommunication qComm; - - private int id; - private Double measured = 0.0; - private Double setpoint = 0.0; - private Integer mode = 0; - - @Nullable - private QbusThermostatHandler thingHandler; - - QThermostat(int id) { - this.id = id; - } - - /** - * Update all values of the thermostat - * - * @param measured current temperature in 1°C multiples - * @param setpoint the setpoint temperature in 1°C multiples - * @param mode 0="Manueel", 1="Vorst", 2="Economisch", 3="Comfort", 4="Nacht" - */ - public void updateState(Double measured, Double setpoint, Integer mode) { - setMeasured(measured); - setSetpoint(setpoint); - setMode(mode); - - QbusThermostatHandler handler = thingHandler; - if (handler != null) { - logger.debug("Qbus: update channels for {}", id); - handler.handleStateUpdate(this); - } - } - - /** - * This method should be called if the ThingHandler for the thing corresponding to the termostat is initialized. - * It keeps a record of the thing handler in this object so the thing can be updated when - * the thermostat receives an update from the Qbus IP-interface. - * - * @param handler - */ - public void setThingHandler(QbusThermostatHandler handler) { - this.thingHandler = handler; - } - - /** - * This method sets a pointer to the qComm object of class {@link QbusCommuncation}. - * This is then used to be able to call back the sendCommand method in this class to send a command to the - * Qbus IP-interface when.. - * - * @param qComm - */ - public void setQComm(QbusCommunication qComm) { - this.qComm = qComm; - } - - /** - * Get measured temperature. - * - * @return measured temperature in 0.5°C multiples - */ - public double getMeasured() { - return this.measured; - } - - /** - * Set measured temperature. - * - * @param measured - */ - private void setMeasured(Double measured) { - this.measured = measured; - } - - /** - * Get setpoint - * - * @return the setpoint temperature in 1°C multiples - */ - public double getSetpoint() { - return this.setpoint; - } - - /** - * Set setpoint temperature. - * - * @param setpoint - */ - private void setSetpoint(Double setpoint) { - this.setpoint = setpoint; - } - - /** - * Get the thermostat mode. - * - * @return the mode: 0="Manueel", 1="Vorst", 2="Economisch", 3="Comfort", 4="Nacht" - */ - public Integer getMode() { - return mode; - } - - /** - * Set the thermostat - * - * mode: 0="Manueel", 1="Vorst", 2="Economisch", 3="Comfort", 4="Nacht" - */ - private void setMode(Integer mode) { - this.mode = mode; - } - - /** - * Sends thermostat mode to Qbus. - * - * @param mode - * @param sn - */ - public void executeMode(int mode, String sn) { - logger.debug("Qbus: execute thermostat mode {} for {} on {}", mode, this.id, sn); - - QMessageCmd qCmd = new QMessageCmd("executethermostat", this.id).withMode(mode).withSn(sn); - - QbusCommunication comm = qComm; - if (comm != null) { - comm.sendMessage(qCmd); - } - } - - /** - * Sends setpoint to Qbus. - * - * @param d - */ - public void executeSetpoint(double d, String sn) { - logger.debug("Qbus: execute thermostat setpoint {} for {} on {}", d, this.id, sn); - - QMessageCmd qCmd = new QMessageCmd("executethermostat", this.id).withSetpoint(d).withSn(sn); - - QbusCommunication comm = qComm; - if (comm != null) { - comm.sendMessage(qCmd); - } - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java deleted file mode 100644 index 52547826ee8f6..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.protocol; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.qbus.internal.handler.QbusBistabielHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link QbusBistabiel} class represents the Qbus BISTABIEL output. - * - * @author Koen Schockaert - Initial Contribution - */ -@NonNullByDefault -public final class QbusBistabiel { - - private final Logger logger = LoggerFactory.getLogger(QbusBistabiel.class); - - @Nullable - private QbusCommunication QComm; - - private int id; - private Integer state = 0; - - @Nullable - private QbusBistabielHandler thingHandler; - - QbusBistabiel(int id) { - this.id = id; - } - - /** - * This method should be called if the ThingHandler for the thing corresponding to this bistabiel is initialized. - * It keeps a record of the thing handler in this object so the thing can be updated when - * the bistable output receives an update from the Qbus IP-interface. - * - * @param handler - */ - public void setThingHandler(QbusBistabielHandler handler) { - this.thingHandler = handler; - } - - /** - * This method sets a pointer to the QComm BISTABIEL of class {@link QbusCommuncation}. - * This is then used to be able to call back the sendCommand method in this class to send a command to the - * Qbus IP-interface when.. - * - * @param QComm - */ - public void setQComm(QbusCommunication QComm) { - this.QComm = QComm; - } - - /** - * Get state of bistabiel. - * - * @return bistabiel state - */ - public Integer getState() { - return this.state; - } - - /** - * Sets state of bistabiel. - * - * @param bistabiel state - */ - void setState(int state) { - this.state = state; - QbusBistabielHandler handler = thingHandler; - if (handler != null) { - logger.debug("Qbus: update bistabiel channel state for {} with {}", id, state); - handler.handleStateUpdate(this); - } - } - - /** - * Sends bistabiel to Qbus. - */ - public void execute(int value, String sn) { - - logger.debug("Qbus: execute bistabiel for {} on CTD {}", this.id, sn); - - QMessageCmd QCmd = new QMessageCmd("executebistabiel", this.id, value).withSn(sn); - - QbusCommunication comm = QComm; - if (comm != null) { - comm.sendMessage(QCmd); - } - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java deleted file mode 100644 index b1aa025c16680..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ - -package org.openhab.binding.qbus.internal.protocol; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.qbus.internal.handler.QbusCO2Handler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link QbusCO2} class represents the action Qbus CO2 output. - * - * @author Koen Schockaert - Initial Contribution - */ -@NonNullByDefault -public final class QbusCO2 { - - private final Logger logger = LoggerFactory.getLogger(QbusCO2.class); - - private int id; - private Integer state = 0; - - @Nullable - private QbusCO2Handler thingHandler; - - QbusCO2(int id) { - this.id = id; - } - - /** - * This method should be called if the ThingHandler for the thing corresponding to this CO2 is initialized. - * It keeps a record of the thing handler in this object so the thing can be updated when - * the CO2 output receives an update from the Qbus IP-interface. - * - * @param handler - */ - public void setThingHandler(QbusCO2Handler handler) { - this.thingHandler = handler; - } - - /** - * Get state of CO2. - * - * @return CO2 state - */ - public Integer getState() { - return this.state; - } - - /** - * Sets state of CO2. - * - * @param CO2 state - */ - public void setState(Integer co2) { - this.state = co2; - QbusCO2Handler handler = thingHandler; - if (handler != null) { - logger.debug("Qbus: update channel state for {} with {}", id, state); - handler.handleStateUpdate(this); - } - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java deleted file mode 100644 index 47c2a115ffedc..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java +++ /dev/null @@ -1,936 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.protocol; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.PrintWriter; -import java.net.InetAddress; -import java.net.Socket; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.qbus.internal.QbusBridgeHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import com.google.gson.JsonParseException; - -/** - * The {@link QbusCommunication} class is able to do the following tasks with Qbus - * systems: - *

    - *
  • Start and stop TCP socket connection with Qbus Server. - *
  • Read all setup and status information from the Qbus Controller. - *
  • Execute Qbus commands. - *
  • Listen to events from Qbus. - *
- * - * A class instance is instantiated from the {@link QbusBridgeHandler} class initialization. - * - * @author Koen Schockaert - Initial Contribution - */ -@NonNullByDefault -public final class QbusCommunication { - - private final Logger logger = LoggerFactory.getLogger(QbusCommunication.class); - - @Nullable - private Socket qSocket; - @Nullable - private PrintWriter qOut; - @Nullable - private BufferedReader qIn; - - private boolean listenerStopped; - private boolean qEventsRunning; - - private Gson gsonOut = new Gson(); - private Gson gsonIn; - - private final Map scenes = new HashMap<>(); - private final Map bistabiel = new HashMap<>(); - private final Map dimmer = new HashMap<>(); - private final Map thermostats = new HashMap<>(); - private final Map co2 = new HashMap<>(); - private final Map Rol = new HashMap<>(); - // private final Map Disconnect = new HashMap<>(); - - @Nullable - private QbusBridgeHandler bridgeCallBack; - - /** - * Constructor for Qbus communication object, manages communication with - * Qbus Server. - * - */ - public QbusCommunication() { - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.registerTypeAdapter(QbusMessageBase.class, new QbusMessageDeserializer()); - this.gsonIn = gsonBuilder.create(); - } - - /** - * Start communication with Qbus Server, run through initialization and start thread listening - * to all messages coming from Qbus. - * - * @param addr : IP-address of Qbus Server - * @param port : Communication port of Qbus server - * @param sn : Serial number of the controller - * - */ - public synchronized void startCommunication() { - QbusBridgeHandler handler = this.bridgeCallBack; - - try { - for (int i = 1; qEventsRunning && (i <= 5); i++) { - Thread.sleep(1000); - } - if (qEventsRunning) { - logger.error("Qbus: starting from thread {}, but previous connection still active after 5000ms", - Thread.currentThread().getId()); - throw new IOException(); - } - - if (handler == null) { - throw new IOException(); - } - - InetAddress addr = handler.getAddr(); - int port = handler.getPort(); - - Socket socket = new Socket(addr, port); - this.qSocket = socket; - this.qOut = new PrintWriter(socket.getOutputStream(), true); - this.qIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); - logger.debug("Qbus: connected via local port {} from thread {}", socket.getLocalPort(), - Thread.currentThread().getId()); - - initialize(); - - (new Thread(qEvents)).start(); - - } catch (IOException | InterruptedException e) { - logger.warn("Qbus: error initializing communication from thread {}", Thread.currentThread().getId()); - if (handler != null) { - handler.bridgeOffline(); - } - stopCommunication(); - - } - } - - /** - * Cleanup socket when the communication with Qbus Server is closed. - * - * @throws IOException - * - */ - public synchronized void stopCommunication() { - this.listenerStopped = true; - - Socket socket = this.qSocket; - - if (socket != null) { - try { - socket.close(); - } catch (IOException ignore) { - // ignore IO Error when trying to close the socket if the intention is to close it anyway - } - } - - this.qSocket = null; - // restartCommunication(); - logger.debug("Qbus: communication stopped from thread {}", Thread.currentThread().getId()); - } - - /** - * Close and restart communication with Qbus Server. - * - */ - public synchronized void restartCommunication() { - QbusBridgeHandler handler = this.bridgeCallBack; - stopCommunication(); - - // handler.bridgeOffline(); - - if (handler != null) { - handler.bridgeOffline(); - } - - logger.debug("Qbus: restart communication from thread {}", Thread.currentThread().getId()); - - startCommunication(); - } - - /** - * Method to check if communication with Qbus Server is active - * - * @return True if active - */ - public boolean communicationActive() { - return (this.qSocket != null); - } - - /** - * Runnable that handles inbound communication from Qbus server. - *

- * The thread listens to the TCP socket opened at instantiation of the {@link QbusCommunication} class - * and interprets all inbound json messages. It triggers state updates for active channels linked to the - * Qbus outputs. It is started after initialization of the communication. - * - */ - private Runnable qEvents = () -> { - String qMessage; - // QbusBridgeHandler handler = this.bridgeCallBack; - - logger.debug("Qbus: listening for events on thread {}", Thread.currentThread().getId()); - listenerStopped = false; - qEventsRunning = true; - - BufferedReader reader = this.qIn; - - try { - if (reader == null) { - throw new IOException(); - } - while (!listenerStopped & ((qMessage = reader.readLine()) != null)) { - if (qMessage != null) { - readMessage(qMessage); - } - } - } catch (IOException e) { - if (!listenerStopped) { - qEventsRunning = false; - logger.warn("Qbus: IO error in listener on thread {}", Thread.currentThread().getId()); - restartCommunication(); - return; - } - } - - qEventsRunning = false; - - stopCommunication(); - - logger.debug("Qbus: event listener thread stopped on thread {}", Thread.currentThread().getId()); - - }; - - /** - * Method that interprets all feedback from Qbus Server application and calls appropriate handling methods. - * - * @param qMessage message read from Qbus. - */ - private void readMessage(String qMessage) { - logger.debug("Qbus: received json on thread {}", Thread.currentThread().getId()); - - try { - QbusMessageBase qMessageGson = this.gsonIn.fromJson(qMessage, QbusMessageBase.class); - String cmd = ""; - String event = ""; - - @SuppressWarnings("null") - String confsn = this.bridgeCallBack.getSn(); - String sn = qMessageGson.getSn(); - cmd = qMessageGson.getCmd(); - event = qMessageGson.getEvent(); - - // logger.debug(cmd); - // logger.debug(event); - - if (Integer.parseInt(confsn) == Integer.parseInt(sn)) { - // Get the compatible outputs from the Qbus server - if ("listbistabiel".equals(cmd)) { - cmdListBistabiel(((QMessageListMap) qMessageGson).getData()); - } else if ("listdimmers".equals(cmd)) { - cmdListDimmers(((QMessageListMap) qMessageGson).getData()); - } else if (("listthermostats").equals(cmd)) { - cmdListThermostat(((QMessageListMap) qMessageGson).getData()); - } else if (("listscenes").equals(cmd)) { - cmdlistscenes(((QMessageListMap) qMessageGson).getData()); - } else if (("listco2").equals(cmd)) { - cmdlistco2(((QMessageListMap) qMessageGson).getData()); - } else if (("listrol").equals(cmd)) { - cmdlistrol(((QMessageListMap) qMessageGson).getData()); - } else if (("listrol02pslats").equals(cmd)) { - cmdlistrolslats(((QMessageListMap) qMessageGson).getData()); - } - // Commands to execute from openHAB to Qbus - else if ("executebistabiel".equals(cmd)) { - cmdExecuteBistabiel(((QMessageMap) qMessageGson).getData()); - } else if ("executedimmers".equals(cmd)) { - cmdExecuteDimmer(((QMessageMap) qMessageGson).getData()); - } else if ("executethermostat".equals(cmd)) { - cmdExecuteThermostat(((QMessageMap) qMessageGson).getData()); - } else if ("executescene".equals(cmd)) { - cmdExecuteScene(((QMessageMap) qMessageGson).getData()); - } else if ("executeslats".equals(cmd)) { - cmdExecuteSlats(((QMessageMap) qMessageGson).getData()); - } else if ("executerol".equals(cmd)) { - cmdExecuteRol(((QMessageMap) qMessageGson).getData()); - } else if ("executerol02pslats".equals(cmd)) { - cmdExecuteRolslats(((QMessageMap) qMessageGson).getData()); - } - // Incoming commands from Qbus Server to openHAB (event) - else if ("listbistabiel".equals(event)) { - eventListBistabiel(((QMessageListMap) qMessageGson).getData()); - } else if ("listdimmers".equals(event)) { - eventListDimmers(((QMessageListMap) qMessageGson).getData()); - } else if ("listthermostat".equals(event)) { - eventListThermostat(((QMessageListMap) qMessageGson).getData()); - } else if ("listscenes".equals(event)) { - eventListScenes(((QMessageListMap) qMessageGson).getData()); - } else if ("listco2".equals(event)) { - eventListCO2(((QMessageListMap) qMessageGson).getData()); - } else if ("listrol".equals(event)) { - eventListRol(((QMessageListMap) qMessageGson).getData()); - } else if ("listrol02pslats".equals(event)) { - eventListRolslats(((QMessageListMap) qMessageGson).getData()); - } - // - else if ("disconnect".equals(event)) { - eventFunction(((QMessageListMap) qMessageGson).getData()); - } - } - } catch (JsonParseException e) { - logger.debug("Qbus: not acted on unsupported json {}", qMessage); - } - } - - /** - * After setting up the communication with the Qbus Server, send all initialization messages. - *

- * First send connect to connect with the Qbus Server application - * Get request for the Scenes - * Get request for Bistabiel/Timers/Intervals/Mono outputs - * Get request for Dimmers 1T and 2T - * Get request for Thermostats - * Get request for CO2 - * Get request for Shutters - * - * @throws IOException - * @throws InterruptedException - */ - private void initialize() throws IOException, InterruptedException { - Connect(); - sendAndReadMessage("listrol"); - sendAndReadMessage("listrol02pslats"); - sendAndReadMessage("listscenes"); - sendAndReadMessage("listbistabiel"); - sendAndReadMessage("listdimmers"); - sendAndReadMessage("listthermostats"); - sendAndReadMessage("listco2"); - } - - /** - * Initial connection to Qbus Server to open a communication channel - */ - private void Connect() { - @SuppressWarnings("null") - String confsn = this.bridgeCallBack.getSn(); - QMessageCmd qCmd = new QMessageCmd("openHAB").withSn(confsn); - sendMessage(qCmd); - } - - /** - * Send message to Qbus server and read response - */ - private void sendAndReadMessage(String command) throws IOException, InterruptedException { - @SuppressWarnings("null") - String confsn = this.bridgeCallBack.getSn(); - QMessageCmd qCmd = new QMessageCmd(command).withSn(confsn); - - sendMessage(qCmd); - - BufferedReader reader = this.qIn; - if (reader == null) { - throw new IOException("Cannot read from socket, reader not connected."); - } - readMessage(reader.readLine()); - } - - /** - * Get all the scenes from the Qbus server - * - * @param data - */ - private void cmdlistscenes(@Nullable List> data) { - logger.debug("Qbus: Scenes received from Qbus server"); - - if (data != null) { - for (Map scene : data) { - try { - int id = Integer.parseInt(scene.get("id")); - QbusScene Scene = new QbusScene(id); - Scene.setQComm(this); - this.scenes.put(id, Scene); - } catch (Exception e) { - logger.debug("Qbus: Error in json for Scenes"); - } - } - } - } - - /** - * Get all the CO2 outputs from the Qbus server - * - * @param data - */ - private void cmdlistco2(@Nullable List> data) { - logger.debug("Qbus: CO2 received from Qbus server"); - - if (data != null) { - for (Map co2 : data) { - - try { - int id = Integer.parseInt(co2.get("id")); - Integer co = 0; - co = Integer.parseInt(co2.get("value1")); - if (!this.co2.containsKey(id)) { - QbusCO2 CO2 = new QbusCO2(id); - this.co2.put(id, CO2); - this.co2.get(id).setState(co); - } else { - this.co2.get(id).setState(co); - } - } catch (Exception e) { - logger.debug("Qbus: Error in json for CO2"); - } - - } - } - } - - /** - * Get all the Positioning module outputs from the Qbus server - * - * @param data - */ - private void cmdlistrol(@Nullable List> data) { - logger.debug("Qbus: ROL02P received from Qbus server"); - - if (data != null) { - for (Map rol : data) { - int id = 0; - id = Integer.parseInt(rol.get("id")); - Integer rolpos = 0; - // Integer rolposslats = 0; - try { - rolpos = Integer.valueOf(rol.get("value1")); - // rolposslats = Integer.valueOf(rol.get("value2")); - } catch (Exception e) { - logger.debug("Qbus: Error in json for Rollershutter"); - } - - if (!this.Rol.containsKey(id)) { - QbusRol Rol = new QbusRol(id); - Rol.setQComm(this); - this.Rol.put(id, Rol); - this.Rol.get(id).setState(rolpos); - // this.Rol.get(id).setSlats(rolposslats); - } else { - this.Rol.get(id).setState(rolpos); - // this.Rol.get(id).setSlats(rolposslats); - } - } - } - } - - /** - * Get all the Positioning module outputs from the Qbus server - * - * @param data - */ - private void cmdlistrolslats(@Nullable List> data) { - logger.debug("Qbus: ROL02PSLATS received from Qbus server"); - - if (data != null) { - for (Map rol : data) { - int id = 0; - id = Integer.parseInt(rol.get("id")); - Integer rolpos = 0; - Integer rolposslats = 0; - try { - rolpos = Integer.valueOf(rol.get("value1")); - rolposslats = Integer.valueOf(rol.get("value2")); - } catch (Exception e) { - logger.debug("Qbus: Error in json for Rollershutter"); - } - - if (!this.Rol.containsKey(id)) { - QbusRol Rol = new QbusRol(id); - Rol.setQComm(this); - this.Rol.put(id, Rol); - this.Rol.get(id).setState(rolpos); - this.Rol.get(id).setSlats(rolposslats); - } else { - this.Rol.get(id).setState(rolpos); - this.Rol.get(id).setSlats(rolposslats); - } - } - } - } - - /** - * Get all the Bistabiel/Timers/Mono/Interval from the Qbus server - * - * @param data - */ - private void cmdListBistabiel(@Nullable List> data) { - logger.debug("Qbus: Bistabiel/Timers/Monos/Intervals received from Qbus server"); - - if (data != null) { - for (Map bistabiel : data) { - int id = 0; - int state = 0; - id = Integer.parseInt(bistabiel.get("id")); - try { - state = Integer.parseInt(bistabiel.get("value1")); - } catch (Exception e) { - logger.debug("Qbus: Error in json for Bistabiel"); - } - - if (!this.bistabiel.containsKey(id)) { - QbusBistabiel qBistabiel = new QbusBistabiel(id); - qBistabiel.setState(state); - qBistabiel.setQComm(this); - this.bistabiel.put(id, qBistabiel); - } else { - this.bistabiel.get(id).setState(state); - } - } - } - } - - /** - * Get all the Dimmer outputs from the Qbus server - * - * @param data - */ - private void cmdListDimmers(@Nullable List> data) { - logger.debug("Qbus: Dimmers received from the Qbus server"); - - if (data != null) { - for (Map dimmer : data) { - - int id = 0; - int state = 0; - id = Integer.parseInt(dimmer.get("id")); - try { - state = Integer.parseInt(dimmer.get("value1")); - } catch (Exception e) { - logger.debug("Qbus: Error in json for Dimmer"); - } - - if (!this.dimmer.containsKey(id)) { - QbusDimmer qDimmer = new QbusDimmer(id); - qDimmer.setState(state); - qDimmer.setQComm(this); - this.dimmer.put(id, qDimmer); - } else { - this.dimmer.get(id).setState(state); - } - } - } - } - - /** - * Get all the Thermostat outputs from the Qbus server - * - * @param data - */ - private void cmdListThermostat(@Nullable List> data) { - logger.debug("Qbus: thermostats received from the Qbus server"); - - if (data != null) { - for (Map thermostat : data) { - int id = Integer.parseInt(thermostat.get("id")); - Double measured = 0.0; - Double setpoint = 0.0; - int mode = 0; - try { - measured = Double.valueOf(thermostat.get("measured")); - setpoint = Double.valueOf(thermostat.get("setpoint")); - mode = Integer.valueOf(thermostat.get("mode")); - } catch (Exception e) { - logger.debug("Qbus: Error in json for Thermostat"); - } - - if (!this.thermostats.containsKey(id)) { - QThermostat qThermostat = new QThermostat(id); - qThermostat.updateState(measured, setpoint, mode); - qThermostat.setQComm(this); - this.thermostats.put(id, qThermostat); - } else { - this.thermostats.get(id).updateState(measured, setpoint, mode); - } - } - } - } - - /** - * Execute Bistabiel/Timers/Monos/Intervals - * - * @param data - */ - private void cmdExecuteBistabiel(Map data) { - Integer errorCode = Integer.valueOf(data.get("error")); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute bistabiel success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); - } - } - - /** - * Execute Scenes - * - * @param data - */ - private void cmdExecuteScene(Map data) { - Integer errorCode = Integer.valueOf(data.get("error")); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute scene success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); - } - } - - /** - * Execute Dimmers - * - * @param data - */ - private void cmdExecuteDimmer(Map data) { - Integer errorCode = Integer.valueOf(data.get("error")); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute dimmer success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); - } - } - - /** - * Execute Slats - * - * @param data - */ - private void cmdExecuteSlats(Map data) { - Integer errorCode = Integer.valueOf(data.get("error")); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute slats success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); - } - } - - /** - * Execute Shutter - * - * @param data - */ - private void cmdExecuteRol(Map data) { - Integer errorCode = Integer.valueOf(data.get("error")); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute shutter success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); - } - } - - /** - * Execute Shutter with slats - * - * @param data - */ - private void cmdExecuteRolslats(Map data) { - Integer errorCode = Integer.valueOf(data.get("error")); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute shutter with slats success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); - } - } - - /** - * Execute Thermostats - * - * @param data - */ - private void cmdExecuteThermostat(Map data) { - Integer errorCode = Integer.valueOf(data.get("error")); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute thermostat success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); - } - } - - /** - * Event on incomming Bistabiel/Timer/Mono/Interval updates - * - * @param data - */ - private void eventListBistabiel(List> data) { - for (Map bistabiel : data) { - int id = Integer.valueOf(bistabiel.get("id")); - if (!this.bistabiel.containsKey(id)) { - logger.warn("Qbus: bistabiel in controller not known {}", id); - return; - } - Integer state = Integer.valueOf(bistabiel.get("value1")); - logger.debug("Qbus: event execute bistabiel {} with state {}", id, state); - this.bistabiel.get(id).setState(state); - } - } - - /** - * Event on incomming CO2 updates - * - * @param data - */ - private void eventListCO2(List> data) { - for (Map co2 : data) { - int id = Integer.valueOf(co2.get("id")); - if (!this.co2.containsKey(id)) { - logger.warn("Qbus: co2 in controller not known {}", id); - return; - } - Integer state = Integer.valueOf(co2.get("value1")); - logger.debug("Qbus: event execute co2 {} with state {}", id, state); - this.co2.get(id).setState(state); - } - } - - /** - * Event on incomming ROL02P without updates - * - * @param data - */ - private void eventListRol(List> data) { - for (Map rol : data) { - int id = Integer.valueOf(rol.get("id")); - if (!this.Rol.containsKey(id)) { - logger.warn("Qbus: Rol02p in controller not known {}", id); - return; - } - Integer pos = Integer.valueOf(rol.get("pos")); - // Integer slat = Integer.valueOf(rol.get("slats")); - logger.debug("Qbus: event execute Rol02P {} with pos {}", id, pos); - this.Rol.get(id).setState(pos); - // this.Rol.get(id).setSlats(slat); - } - } - - /** - * Event on incomming ROL02P with slats updates - * - * @param data - */ - private void eventListRolslats(List> data) { - for (Map rol : data) { - int id = Integer.valueOf(rol.get("id")); - if (!this.Rol.containsKey(id)) { - logger.warn("Qbus: Rol02p in controller not known {}", id); - return; - } - Integer pos = Integer.valueOf(rol.get("pos")); - Integer slat = Integer.valueOf(rol.get("slats")); - logger.debug("Qbus: event execute ROL02P_Slats {} with pos {} and slats {}", id, pos, slat); - this.Rol.get(id).setState(pos); - this.Rol.get(id).setSlats(slat); - } - } - - /** - * Event on incomming Scene updates - * - * @param data - */ - private void eventListScenes(List> data) { - for (Map scene : data) { - int id = Integer.valueOf(scene.get("id")); - if (!this.scenes.containsKey(id)) { - logger.warn("Qbus: scene in controller not known {}", id); - return; - } - Integer state = Integer.valueOf(scene.get("value1")); - logger.debug("Qbus: event execute scene {} with state {}", id, state); - this.scenes.get(id).setState(state); - } - } - - /** - * Event on incomming Dimmer updates - * - * @param data - */ - private void eventListDimmers(List> data) { - for (Map dimmer : data) { - int id = Integer.valueOf(dimmer.get("id")); - if (!this.dimmer.containsKey(id)) { - logger.warn("Qbus: dimmer in controller not known {}", id); - return; - } - Integer state = Integer.valueOf(dimmer.get("value1")); - logger.debug("Qbus: event execute dimmer {} with state {}", id, state); - this.dimmer.get(id).setState(state); - } - } - - /** - * Event on incomming thermostat updates - * - * @param data - */ - private void eventListThermostat(List> data) { - for (Map thermostat : data) { - int id = Integer.parseInt(thermostat.get("id")); - if (!this.thermostats.containsKey(id)) { - logger.warn("Qbus: thermostat in controller not known {}", id); - return; - } - Double measured = Double.valueOf(thermostat.get("measured")); - Double setpoint = Double.valueOf(thermostat.get("setpoint")); - Integer mode = Integer.valueOf(thermostat.get("mode")); - logger.debug("Qbus: event execute thermostat {} with measured {}, setpoint {}, mode {}", id, measured, - setpoint, mode); - this.thermostats.get(id).updateState(measured, setpoint, mode); - } - } - - /* - * /** - * Event on Disconnect - * - * @param data - */ - private void eventFunction(List> data) { - // for (Map function : data) { - logger.debug("Disconnect"); - // String ctd = function.get("ctd"); - // String funcion = function.get("function"); - - QbusBridgeHandler handler = this.bridgeCallBack; - - if (handler != null) { - stopCommunication(); - handler.bridgeOffline(); - - } - } - - /** - * Called by other methods to send json cmd to Qbus. - * - * @param qMessage - */ - synchronized void sendMessage(Object qMessage) { - PrintWriter writer = this.qOut; - String json = gsonOut.toJson(qMessage); - logger.debug("Qbus: send json from thread {}", Thread.currentThread().getId()); - - if (writer != null) { - writer.println(json); - - try { - TimeUnit.MILLISECONDS.sleep(250); - } catch (InterruptedException e) { - // No reaction on error is required - } - - } - if ((writer == null) || (writer.checkError())) { - logger.warn("Qbus: error sending message, trying to restart communication"); - restartCommunication(); - // retry sending after restart - logger.debug("Qbus: resend json from thread {}", Thread.currentThread().getId()); - writer = this.qOut; - if (writer != null) { - writer.println(json); - } - if ((writer == null) || (writer.checkError())) { - logger.warn("Qbus: error resending message"); - - } - } - } - - /** - * Return all Bistabiel/Timers/Mono/Intervals in the Qbus Controller. - * - * @return - */ - public Map getBistabiel() { - return this.bistabiel; - } - - /** - * Return all Dimmers in the Qbus Controller. - * - * @return - */ - public Map getDimmer() { - return this.dimmer; - } - - /** - * Return all Scenes in the Qbus Controller - * - * @return - */ - public Map getScenes() { - return this.scenes; - } - - /** - * Return all Thermostats in the Qbus Controller. - * - * @return - */ - public Map getThermostats() { - return this.thermostats; - } - - /** - * Return all CO2 in the Qbus Controller. - * - * @return - */ - public Map getCo2() { - return this.co2; - } - - /** - * Return all ROL02P outûts in the Qbus Controller. - * - * @return - */ - public Map getRol() { - return this.Rol; - } - - /** - * @param bridgeCallBack the bridgeCallBack to set - */ - public void setBridgeCallBack(QbusBridgeHandler bridgeCallBack) { - this.bridgeCallBack = bridgeCallBack; - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java deleted file mode 100644 index b7fb67c1ef84f..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.protocol; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.qbus.internal.handler.QbusDimmerHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link QbusDimmer} class represents the action Qbus Dimmer output. - * - * @author Koen Schockaert - Initial Contribution - */ -@NonNullByDefault -public final class QbusDimmer { - - private final Logger logger = LoggerFactory.getLogger(QbusDimmer.class); - - @Nullable - private QbusCommunication QComm; - - private int id; - private Integer state = 0; - - @Nullable - private QbusDimmerHandler thingHandler; - - QbusDimmer(int id) { - this.id = id; - } - - /** - * This method should be called if the ThingHandler for the thing corresponding to this dimmer is initialized. - * It keeps a record of the thing handler in this object so the thing can be updated when - * the dimmer receives an update from the Qbus IP-interface. - * - * @param handler - */ - public void setThingHandler(QbusDimmerHandler handler) { - this.thingHandler = handler; - } - - /** - * This method sets a pointer to the QComm Dimmer of class {@link QbusCommuncation}. - * This is then used to be able to call back the sendCommand method in this class to send a command to the - * Qbus IP-interface when.. - * - * @param QComm - */ - public void setQComm(QbusCommunication QComm) { - this.QComm = QComm; - } - - /** - * Get state of dimmer. - * - * @return dimmer state - */ - public Integer getState() { - return this.state; - } - - /** - * Sets state of Dimmer. - * - * @param dimmer state - */ - void setState(int state) { - this.state = state; - QbusDimmerHandler handler = thingHandler; - if (handler != null) { - logger.debug("Qbus: update channel state for {} with {}", id, state); - handler.handleStateUpdate(this); - } - } - - /** - * Sends Dimmer state to Qbus. - */ - public void execute(int percent, String sn) { - logger.debug("Qbus: execute dimmer for {} on CTD {}", this.id, sn); - - QMessageCmd QCmd = new QMessageCmd("executedimmer", this.id, percent).withSn(sn); - - QbusCommunication comm = QComm; - if (comm != null) { - comm.sendMessage(QCmd); - } - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java deleted file mode 100644 index 6b6b1bf71900a..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java +++ /dev/null @@ -1,54 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.protocol; - -/** - * Class {@link QbusMessageBase} used as base class for output from gson for cmd or event feedback from the Qbus server. - * This class only contains the common base fields required for the deserializer - * {@link QbusMessageDeserializer} to select the specific formats implemented in {@link QbusMessageMap}, - * {@link QbusMessageListMap}, {@link QbusMessageCmd}. - *

- * - * @author Koen Schockaert - Initial Contribution - */ - -abstract class QbusMessageBase { - - private String cmd = ""; - private String event1 = ""; - private String sn = ""; - - String getCmd() { - return this.cmd; - } - - void setCmd(String cmd) { - this.cmd = cmd; - } - - void setSn(String sn) { - this.sn = sn; - } - - String getEvent() { - return this.event1; - } - - void setEvent(String event) { - this.event1 = event; - } - - String getSn() { - return this.sn; - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java deleted file mode 100644 index eee52cd2fd3ca..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java +++ /dev/null @@ -1,110 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.protocol; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; - -import com.google.gson.JsonArray; -import com.google.gson.JsonDeserializationContext; -import com.google.gson.JsonDeserializer; -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonParseException; - -/** - * Class {@link QbusMessageDeserializer} deserializes all json messages from Qbus. Various json - * message formats are supported. The format is selected based on the content of the cmd and event json objects. - * - * @author Koen Schockaert - Initial Contribution - * - */ - -class QbusMessageDeserializer implements JsonDeserializer { - - @Override - public QbusMessageBase deserialize(final JsonElement json, final Type typeOfT, - final JsonDeserializationContext context) throws JsonParseException { - final JsonObject jsonObject = json.getAsJsonObject(); - - try { - - String cmd = null; - String event1 = null; - String sn = null; - - if (jsonObject.has("cmd")) { - cmd = jsonObject.get("cmd").getAsString(); - } - if (jsonObject.has("event1")) { - event1 = jsonObject.get("event1").getAsString(); - } - if (jsonObject.has("ctdsn")) { - sn = jsonObject.get("ctdsn").getAsString(); - } - - JsonElement jsonData = null; - if (jsonObject.has("data")) { - jsonData = jsonObject.get("data"); - } - - QbusMessageBase message = null; - - if (jsonData != null) { - if (jsonData.isJsonObject()) { - message = new QMessageMap(); - - Map data = new HashMap<>(); - for (Entry entry : jsonData.getAsJsonObject().entrySet()) { - data.put(entry.getKey(), entry.getValue().getAsString()); - } - ((QMessageMap) message).setData(data); - - } else if (jsonData.isJsonArray()) { - JsonArray jsonDataArray = jsonData.getAsJsonArray(); - - message = new QMessageListMap(); - - List> dataList = new ArrayList<>(); - for (int i = 0; i < jsonDataArray.size(); i++) { - JsonObject jsonDataObject = jsonDataArray.get(i).getAsJsonObject(); - - Map data = new HashMap<>(); - for (Entry entry : jsonDataObject.entrySet()) { - data.put(entry.getKey(), entry.getValue().getAsString()); - } - dataList.add(data); - } - ((QMessageListMap) message).setData(dataList); - } - } - - if (message != null) { - message.setCmd(cmd); - message.setEvent(event1); - message.setSn(sn); - } else { - throw new JsonParseException("Unexpected Json type"); - } - - return message; - - } catch (IllegalStateException | ClassCastException e) { - throw new JsonParseException("Unexpected Json type"); - } - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java deleted file mode 100644 index 29d4cb6779c41..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java +++ /dev/null @@ -1,141 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.protocol; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.qbus.internal.handler.QbusRolHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link QbusRol} class represents the action Qbus Shutter/Slats output. - * - * @author Koen Schockaert - Initial Contribution - */ -@NonNullByDefault -public final class QbusRol { - - private final Logger logger = LoggerFactory.getLogger(QbusRol.class); - - @Nullable - private QbusCommunication QComm; - - private int id; - private Integer state = 0; - private Integer slats = 0; - - @Nullable - private QbusRolHandler thingHandler; - - QbusRol(int id) { - this.id = id; - } - - /** - * This method should be called if the ThingHandler for the thing corresponding to this Shutter/Slats is - * initialized. - * It keeps a record of the thing handler in this object so the thing can be updated when - * the shutter/slat receives an update from the Qbus IP-interface. - * - * @param qbusRolHandler - */ - public void setThingHandler(QbusRolHandler qbusRolHandler) { - this.thingHandler = qbusRolHandler; - } - - /** - * This method sets a pointer to the QComm Shutter/Slats of class {@link QbusCommuncation}. - * This is then used to be able to call back the sendCommand method in this class to send a command to the - * Qbus IP-interface when.. - * - * @param QComm - */ - public void setQComm(QbusCommunication QComm) { - this.QComm = QComm; - } - - /** - * Get state of shutter. - * - * @return shutter state - */ - public Integer getState() { - return this.state; - } - - /** - * Get state of slats. - * - * @return slats state - */ - public Integer getStateSlats() { - return this.slats; - } - - /** - * Sets state of Shutter. - * - * @param shutter state - */ - public void setState(Integer Slats) { - this.state = Slats; - QbusRolHandler handler = thingHandler; - if (handler != null) { - logger.debug("Qbus: update channel shutter for {} with {}", id, state); - handler.handleStateUpdate(this); - } - } - - /** - * Sets state of Slats. - * - * @param slats state - */ - public void setSlats(Integer Slats) { - this.slats = Slats; - QbusRolHandler handler = thingHandler; - if (handler != null) { - logger.debug("Qbus: update channel slats for {} with {}", id, slats); - handler.handleStateUpdate(this); - } - } - - /** - * Sends shutter to Qbus. - */ - public void execute(int percent, String sn) { - logger.debug("Qbus: execute position for {} {}", this.id, sn); - - QMessageCmd QCmd = new QMessageCmd("executestore", this.id, percent).withSn(sn); - - QbusCommunication comm = QComm; - if (comm != null) { - comm.sendMessage(QCmd); - } - } - - /** - * Sends slats to Qbus. - */ - public void executeSlats(int percent, String sn) { - logger.debug("Qbus: execute slats for {} {}", this.id, sn); - - QMessageCmd QCmd = new QMessageCmd("executeslats", this.id, percent).withSn(sn); - - QbusCommunication comm = QComm; - if (comm != null) { - comm.sendMessage(QCmd); - } - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java deleted file mode 100644 index 6b58b37da3157..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.protocol; - -import org.eclipse.jdt.annotation.NonNullByDefault; -import org.eclipse.jdt.annotation.Nullable; -import org.openhab.binding.qbus.internal.handler.QbusSceneHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -/** - * The {@link QbusScene} class represents the action Qbus Scene output. - * - * @author Koen Schockaert - Initial Contribution - */ -@NonNullByDefault -public final class QbusScene { - - private final Logger logger = LoggerFactory.getLogger(QbusScene.class); - - @Nullable - private QbusCommunication QComm; - - private int id; - private Integer state = 0; - - @Nullable - private QbusSceneHandler thingHandler; - - QbusScene(int id) { - this.id = id; - } - - /** - * This method should be called if the ThingHandler for the thing corresponding to this scene is initialized. - * It keeps a record of the thing handler in this object so the thing can be updated when - * the scene receives an update from the Qbus IP-interface. - * - * @param handler - */ - public void setThingHandler(QbusSceneHandler handler) { - this.thingHandler = handler; - } - - /** - * This method sets a pointer to the QComm Scene of class {@link QbusCommuncation}. - * This is then used to be able to call back the sendCommand method in this class to send a command to the - * Qbus IP-interface when.. - * - * @param QComm - */ - public void setQComm(QbusCommunication QComm) { - this.QComm = QComm; - } - - /** - * Get state of scene. - * - * @return scene state - */ - public Integer getState() { - return this.state; - } - - /** - * Sets state of Scene. - * - * @param scene state - */ - void setState(int state) { - this.state = state; - QbusSceneHandler handler = thingHandler; - if (handler != null) { - logger.debug("Qbus: update channel state for {} with {}", id, state); - handler.handleStateUpdate(this); - } - } - - /** - * Sends action to Qbus. - */ - public void execute(int val, String sn) { - logger.debug("Qbus: execute scene for {} on CTD {}", this.id, sn); - - QMessageCmd QCmd = new QMessageCmd("executescene", this.id, val).withSn(sn); - ; - - QbusCommunication comm = QComm; - if (comm != null) { - comm.sendMessage(QCmd); - } - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml deleted file mode 100644 index b18daa90afc8d..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - Qbus Binding - This is the binding for Qbus. - Koen Schockaert - - diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties deleted file mode 100644 index 0c7193b3f16c7..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties +++ /dev/null @@ -1,17 +0,0 @@ -# FIXME: please substitute the xx_XX with a proper locale, ie. de_DE -# FIXME: please do not add the file to the repo if you add or change no content -# binding -binding.qbus.name = -binding.qbus.description = - -# thing types -thing-type.qbus.sample.label = -thing-type.qbus.sample.description = - -# thing type config description -thing-type.config.qbus.sample.config1.label = -thing-type.config.qbus.sample.config1.description = - -# channel types -channel-type.qbus.sample-channel.label = -channel-type.qbus.sample-channel.description = diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml deleted file mode 100644 index a7aef153dc7e5..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml +++ /dev/null @@ -1,247 +0,0 @@ - - - - - - This bridge represents a Qbus client - - - - IP Address of Qbus server, usually 'localhost' - localhost - false - network-address - - - - Serial nr of the CTD controller - - false - - - - Port to communicate with Qbus server, default 8447 - 8447 - true - - - - Refresh interval for connection with Qbus server (min), default 300. If set to 0 or left empty, no - refresh will be scheduled - 300 - true - - - - - - - - - - Bistabiel-Mono-Timer-Interval output - - - - - - - Qbus IP Interface Output Object ID - false - - - - - - - - - - Qbus scene - - - - - - - Qbus scene ID - false - - - - - - - - - - - Qbus CO2 - - - - - - - Qbus CO2 ID - false - - - - - - - - - - Qbus dimmer output - - - - - - - Qbus IP Interface Output ID - false - - - - Step value used for increase/decrease of dimmer brightness, default 10% - 10 - true - - - - - - - - - - - - Qbus Shutter control - - - - - - - - Qbus rol Id - false - - - - - - - - - - Qbus thermostat - - - - - - - - - Qbus IP Interface Thermostat ID - false - - - - - - Switch - - Scene control for Qbus - Scene - - - - Number - - CO2 value for Qbus - CO2 - - - - Switch - - Switch control for output in Qbus - Switch - - - - Dimmer - - Brightness control for dimmer function in Qbus - DimmableLight - - - - Color - - DMX control for dimmer function in Qbus - HSB - - - - Number:Temperature - - Temperature measured by thermostat - Temperature - - CurrentTemperature - - - - - - Number:Temperature - - Setpoint temperature of thermostat - Temperature - - TargetTemperature - - - - - - Number - - Thermostat mode - Number - - - - - - - - - - - - - Rollershutter - - Rollershutter control for Qbus - Blinds - - - - Dimmer - - Slatcontrol function in Qbus - Blinds - - - From 8342d4e6ac94918bd2c498246ca3ddc155b98500 Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Mon, 30 Nov 2020 10:31:19 +0100 Subject: [PATCH 03/17] First Commit Signed-off-by: QbusKoen --- bundles/org.openhab.binding.qbus/NOTICE | 13 + bundles/org.openhab.binding.qbus/README.md | 103 ++ bundles/org.openhab.binding.qbus/doc/Logo.JPG | Bin 0 -> 12981 bytes bundles/org.openhab.binding.qbus/pom.xml | 17 + .../src/main/feature/feature.xml | 23 + .../qbus/internal/QbusBindingConstants.java | 98 ++ .../qbus/internal/QbusBridgeHandler.java | 241 +++++ .../qbus/internal/QbusHandlerFactory.java | 69 ++ .../handler/QbusBistabielHandler.java | 183 ++++ .../qbus/internal/handler/QbusCO2Handler.java | 167 ++++ .../internal/handler/QbusDimmerHandler.java | 250 +++++ .../qbus/internal/handler/QbusRolHandler.java | 264 +++++ .../internal/handler/QbusSceneHandler.java | 181 ++++ .../handler/QbusThermostatHandler.java | 183 ++++ .../qbus/internal/protocol/QMessageCmd.java | 75 ++ .../internal/protocol/QMessageListMap.java | 38 + .../qbus/internal/protocol/QMessageMap.java | 36 + .../qbus/internal/protocol/QThermostat.java | 175 ++++ .../qbus/internal/protocol/QbusBistabiel.java | 103 ++ .../qbus/internal/protocol/QbusCO2.java | 75 ++ .../internal/protocol/QbusCommunication.java | 936 ++++++++++++++++++ .../qbus/internal/protocol/QbusDimmer.java | 102 ++ .../internal/protocol/QbusMessageBase.java | 54 + .../protocol/QbusMessageDeserializer.java | 110 ++ .../qbus/internal/protocol/QbusRol.java | 141 +++ .../qbus/internal/protocol/QbusScene.java | 103 ++ .../main/resources/OH-INF/binding/binding.xml | 10 + .../OH-INF/i18n/qbus_xx_XX.properties | 17 + .../resources/OH-INF/thing/thing-types.xml | 247 +++++ 29 files changed, 4014 insertions(+) create mode 100644 bundles/org.openhab.binding.qbus/NOTICE create mode 100644 bundles/org.openhab.binding.qbus/README.md create mode 100644 bundles/org.openhab.binding.qbus/doc/Logo.JPG create mode 100644 bundles/org.openhab.binding.qbus/pom.xml create mode 100644 bundles/org.openhab.binding.qbus/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageListMap.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageMap.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QThermostat.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties create mode 100644 bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml diff --git a/bundles/org.openhab.binding.qbus/NOTICE b/bundles/org.openhab.binding.qbus/NOTICE new file mode 100644 index 0000000000000..38d625e349232 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/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.qbus/README.md b/bundles/org.openhab.binding.qbus/README.md new file mode 100644 index 0000000000000..89b5efe07d84d --- /dev/null +++ b/bundles/org.openhab.binding.qbus/README.md @@ -0,0 +1,103 @@ +# Qbus Binding +![Qbus Logo](doc/Logo.JPG) + +This binding for Qbus communicates for all controllers of the Qbus home automation system. + +More information about Qbus can be found here: +[Qbus](https://qbus.be) + +This is the link to the [Qbus forum](https://qbusforum.be). This forum is mainly in dutch and you can find a lot of information about the pre testings af this binding and offers a way to communicate with other users. + +We also host a site which contains a [manual](https://manualoh.schockaert.tk/) where you can find lot's of information. + +The controllers can not communicate directly with openHAB, therefore we developed a Client/Server application which you must install prior to enable this binding. +More information can be found here: +[Qbus Client/Server](https://github.com/QbusKoen/QbusClientServer) + +With this binding you can control and read almost every output from the Qbus system. + +## Supported Things + +The following things are supported by the Qbus Binding: +- Dimmer 1 button, 2 button and clc as _dimmer_ +- Bistabiel, Timer1-3, Interval as _onOff_ +- Thermostats - normal and PID as _thermosats_ +- Scenes as _scene_ +- CO2 as _co2_ +- Rollershutter and rollerhutter with slats as _rollershutter_ + +For now the folowing Qbus things are not yet supported but will come: +- DMX +- Timer 4 & 5 +- HVAC +- Humidity +- Renson +- Duco +- Kinetura +- Energy monitor +- Weather station + + +## Discovery + +The discovery service is not yet implemented but the System Manager III software of Qbus generates things and item files from the programming, which you can use directly in openHAB. + +## Thing Configuration + +This is an example of the things configuration file: + +``` +Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, refresh=10 ] { + dimmer 1 "ToonzaalLED" [ dimmerId=100 ] + onOff 30 "Toonzaal230V" [ bistabielId=76 ] + thermostat 50 "Service" [ thermostatId=99 ] + scene 70 "Disco" [ sceneId=36 ] + co2 100 "Productie" [ co2Id=26 ] + rollershutter 120 "Roller1" [ rolId=268 ] + rollershutter_slats 121 "Roller2" [ rolId=264 ] +} +``` +The Bridge connects to the QbusServer, so if the Client/Server application is installed on the same machine then localhost can be used as address. sn is the serial nr of your controller, port should allways be 8447, except in special installations as it is the communication port of the Server application. refresh is a time in minutes which will check the status of the server and reconnects if connection is broken after this time. + +## Channels + +| channel | type | description | +|---------------------|---------------|---------------------------------------------------------| +| onOff | switch | This is the channel for Bistable, Timers and Intervals | +| dimmer | brightness | This is the channel for Dimmers 1&2 buttons and CLC | +| scene | Switch | This is the channel for scenes | +| co2 | co2 | This is the channel for CO2 sensors | +| rollershutter | rollershutter | This is the channel for rollershutters | +| rollershutter_slats | rollershutter | This is the channel for rollershutters with slats | +| thermostat | setpoint | This is the channel for thermostats setpoint | +| thermostat | measured | This is the channel for thermostats currenttemp | +| thermostat | mode | This is the channel for thermostats mode | + + +## Full Example + +### Things: +``` +Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, refresh=10 ] { + dimmer 1 "ToonzaalLED" [ dimmerId=100 ] + onOff 30 "Toonzaal230V" [ bistabielId=76 ] + thermostat 50 "Service" [ thermostatId=99 ] + scene 70 "Disco" [ sceneId=36 ] + co2 100 "Productie" [ co2Id=26 ] + rollershutter 120 "Roller1" [ rolId=268 ] + rollershutter_slats 121 "Roller2" [ rolId=264 ] +} +``` +### Items: +``` +Dimmer ToonzaalLED [ "Lighting" ] {channel="qbus:dimmer:CTD007841:1:brightness"} +Switch Toonzaal230V {channel="qbus:onOff:CTD007841:30:switch"} +Number:Temperature ServiceSP"[%.1f °C]" (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:setpoint"} +Number:Temperature ServiceCT"[%.1f °C]" (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:measured"} +Number ServiceMode (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:mode",ihc="0x33c311" , autoupdate="true"} +Switch Disco {channel="qbus:scene:CTD007841:36:scene"} +Number ProductieCO2 {channel="qbus:co2:CTD007841:100:co2"} +Rollershutter Roller1 {channel="qbus:rollershutter:CTD007841:120:rollershutter"} +Rollershutter Roller2 {channel="qbus:rollershutter_slats:CTD007841:121:rollershutter"} +Dimmer Roller2_slats {channel="qbus:rollershutter_slats:CTD007841:121:slats"} +``` \ No newline at end of file diff --git a/bundles/org.openhab.binding.qbus/doc/Logo.JPG b/bundles/org.openhab.binding.qbus/doc/Logo.JPG new file mode 100644 index 0000000000000000000000000000000000000000..4db5d75ae4403d9d1aa945287c333e9dc85d6130 GIT binary patch literal 12981 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 literal 0 HcmV?d00001 diff --git a/bundles/org.openhab.binding.qbus/pom.xml b/bundles/org.openhab.binding.qbus/pom.xml new file mode 100644 index 0000000000000..3cda2c4358f52 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.0.0-SNAPSHOT + + + org.openhab.binding.qbus + + openHAB Add-ons :: Bundles :: Qbus Binding + + diff --git a/bundles/org.openhab.binding.qbus/src/main/feature/feature.xml b/bundles/org.openhab.binding.qbus/src/main/feature/feature.xml new file mode 100644 index 0000000000000..fcae4ee3aee20 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/feature/feature.xml @@ -0,0 +1,23 @@ + + + + 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.qbus/${project.version} + + diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java new file mode 100644 index 0000000000000..5cd8e6bc2b10f --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java @@ -0,0 +1,98 @@ +/** + * 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.qbus.internal; + +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.openhab.core.thing.ThingTypeUID; + +/** + * The {@link QbusBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Koen Schockaert - Initial contribution + */ +@NonNullByDefault +public class QbusBindingConstants { + + private static final String BINDING_ID = "qbus"; + + // bridge + public static final ThingTypeUID BRIDGE_THING_TYPE = new ThingTypeUID(BINDING_ID, "bridge"); + public static final Set BRIDGE_THING_TYPES_UIDS = Collections.singleton(BRIDGE_THING_TYPE); + // Bridge config properties + public static final String CONFIG_HOST_NAME = "addr"; + public static final String CONFIG_PORT = "port"; + public static final String CONFIG_REFRESH = "refresh"; + public static final String CONFIG_SN = "sn"; + + // generic thing types + public static final ThingTypeUID THING_TYPE_CO2 = new ThingTypeUID(BINDING_ID, "co2"); + public static final ThingTypeUID THING_TYPE_SCENE = new ThingTypeUID(BINDING_ID, "scene"); + public static final ThingTypeUID THING_TYPE_TIMER_LIGHT = new ThingTypeUID(BINDING_ID, "onOff"); + public static final ThingTypeUID THING_TYPE_ON_OFF_LIGHT = new ThingTypeUID(BINDING_ID, "onOff"); + public static final ThingTypeUID THING_TYPE_DIMMABLE_LIGHT = new ThingTypeUID(BINDING_ID, "dimmer"); + public static final ThingTypeUID THING_TYPE_ROLLERSHUTTER = new ThingTypeUID(BINDING_ID, "rollershutter"); + public static final ThingTypeUID THING_TYPE_ROLLERSHUTTER_SLATS = new ThingTypeUID(BINDING_ID, + "rollershutter_slats"); + public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat"); + // List of all Thing Type UIDs + public static final Set SCENE_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_SCENE).collect(Collectors.toSet())); + + public static final Set CO2_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_CO2).collect(Collectors.toSet())); + + public static final Set ROLLERSHUTTER_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_ROLLERSHUTTER).collect(Collectors.toSet())); + + public static final Set ROLLERSHUTTER_SLATS_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_ROLLERSHUTTER_SLATS).collect(Collectors.toSet())); + + public static final Set BISTABIEL_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_ON_OFF_LIGHT, THING_TYPE_TIMER_LIGHT).collect(Collectors.toSet())); + + public static final Set THERMOSTAT_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_THERMOSTAT).collect(Collectors.toSet())); + + public static final Set DIMMER_THING_TYPES_UIDS = Collections + .unmodifiableSet(Stream.of(THING_TYPE_ON_OFF_LIGHT, THING_TYPE_DIMMABLE_LIGHT).collect(Collectors.toSet())); + + public static final Set SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(Stream + .of(THING_TYPE_TIMER_LIGHT, THING_TYPE_ON_OFF_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_THERMOSTAT, + THING_TYPE_SCENE, THING_TYPE_CO2, THING_TYPE_ROLLERSHUTTER, THING_TYPE_ROLLERSHUTTER_SLATS) + .collect(Collectors.toSet())); + + // List of all Channel ids + public static final String CHANNEL_SWITCH = "switch"; + public static final String CHANNEL_BRIGHTNESS = "brightness"; + public static final String CHANNEL_MEASURED = "measured"; + public static final String CHANNEL_SETPOINT = "setpoint"; + public static final String CHANNEL_MODE = "mode"; + public static final String CHANNEL_CO2 = "co2"; + public static final String CHANNEL_ROLLERSHUTTER = "rollershutter"; + public static final String CHANNEL_SLATS = "slats"; + + // Thing config properties + public static final String CONFIG_BISTABIEL_ID = "bistabielId"; + public static final String CONFIG_DIMMER_ID = "dimmerId"; + public static final String CONFIG_THERMOSTAT_ID = "thermostatId"; + public static final String CONFIG_SCENE_ID = "sceneId"; + public static final String CONFIG_CO2_ID = "co2Id"; + public static final String CONFIG_ROLLERSHUTTER_ID = "rolId"; + public static final String CONFIG_STEP_VALUE = "step"; +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java new file mode 100644 index 0000000000000..4377a1fd5803f --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java @@ -0,0 +1,241 @@ +/** + * 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.qbus.internal; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; + +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseBridgeHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * {@link QbusBridgeHandler} is the handler for a Qbus controller + * + * @author Koen Schockaert - Initial Contribution + */ +public class QbusBridgeHandler extends BaseBridgeHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusBridgeHandler.class); + + private QbusCommunication qbusComm; + + private ScheduledFuture refreshTimer; + + public QbusBridgeHandler(Bridge Bridge) { + super(Bridge); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // There is nothing to handle in the bridge handler + } + + @Override + public void initialize() { + logger.debug("QBUS: initializing bridge handler"); + + Configuration config = this.getConfig(); + InetAddress addr = getAddr(); + int port = getPort(); + + logger.debug("Qbus: bridge handler host {}, port {}", addr, port); + + if (addr != null) { + createCommunicationObject(addr, port); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Qbus: cannot resolve bridge IP with hostname " + config.get(CONFIG_HOST_NAME)); + } + } + + /** + * Create communication object to Qbus server and start communication. + * + * @param addr : IP address of Qbus server + * @param port : Communication port of QbusServer + * @param sn : Serial number of Controller + */ + private void createCommunicationObject(InetAddress addr, int port) { + Configuration config = this.getConfig(); + scheduler.submit(() -> { + qbusComm = new QbusCommunication(); + + // Set callback from Qbus object to this bridge to be able to take bridge + // offline when non-resolvable communication error occurs. + setBridgeCallBack(); + + qbusComm.startCommunication(); + if (!qbusComm.communicationActive()) { + qbusComm = null; + bridgeOffline(); + return; + } + + updateStatus(ThingStatus.ONLINE); + + Integer refreshInterval = ((Number) config.get(CONFIG_REFRESH)).intValue(); + setupRefreshTimer(refreshInterval); + + }); + } + + private void setBridgeCallBack() { + this.qbusComm.setBridgeCallBack(this); + } + + /** + * Schedule future communication refresh. + * + * @param interval_config Time before refresh in minutes. + */ + private void setupRefreshTimer(Integer refreshInterval) { + if (this.refreshTimer != null) { + this.refreshTimer.cancel(true); + this.refreshTimer = null; + } + + if ((refreshInterval == null) || (refreshInterval == 0)) { + return; + } + + // This timer will restart the bridge connection periodically + logger.debug("Qbus: Checking for Client communication every {} min", refreshInterval); + this.refreshTimer = scheduler.scheduleWithFixedDelay(() -> { + logger.debug("Qbus: check communication after timerinterval"); + + if (!qbusComm.communicationActive()) { + logger.debug("Qbus: Restarting communication"); + qbusComm.restartCommunication(); + + if (!qbusComm.communicationActive()) { + qbusComm = null; + bridgeOffline(); + updateStatus(ThingStatus.OFFLINE); + return; + } + + // updateStatus(ThingStatus.ONLINE); + updateStatus(ThingStatus.ONLINE); + } else { + logger.debug("Qbus: Communication still active"); + } + + }, refreshInterval, refreshInterval, TimeUnit.MINUTES); + } + + /** + * Take bridge offline when error in communication with Qbus server. This method can also be + * called directly from {@link QbusCommunication} object. + */ + public void bridgeOffline() { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, + "Qbus: error starting bridge connection"); + } + + /** + * Put bridge online when error in communication resolved. + */ + public void bridgeOnline() { + updateStatus(ThingStatus.ONLINE); + } + + @Override + public boolean isInitialized() { + return true; + } + + @Override + public void dispose() { + if (this.refreshTimer != null) { + this.refreshTimer.cancel(true); + } + this.refreshTimer = null; + } + + @Override + public void handleConfigurationUpdate(Map configurationParameters) { + Configuration configuration = editConfiguration(); + for (Entry configurationParmeter : configurationParameters.entrySet()) { + configuration.put(configurationParmeter.getKey(), configurationParmeter.getValue()); + } + updateConfiguration(configuration); + + scheduler.submit(() -> { + + updateStatus(ThingStatus.ONLINE); + + Integer refreshInterval = ((Number) configuration.get(CONFIG_REFRESH)).intValue(); + setupRefreshTimer(refreshInterval); + }); + } + + /** + * Get the Qbus communication object. + * + * @return Qbus communication object + */ + public QbusCommunication getCommunication() { + return this.qbusComm; + } + + /** + * Get the IP-address of the Qbus server. + * + * @return the addr + */ + public InetAddress getAddr() { + Configuration config = this.getConfig(); + InetAddress addr = null; + try { + addr = InetAddress.getByName((String) config.get(CONFIG_HOST_NAME)); + } catch (UnknownHostException e) { + logger.debug("Qbus: Cannot resolve hostname {} to IP adress", config.get(CONFIG_HOST_NAME)); + } + return addr; + } + + /** + * Get the listening port of the Qbus server. + * + * @return the port + */ + public int getPort() { + Configuration config = this.getConfig(); + return ((Number) config.get(CONFIG_PORT)).intValue(); + } + + /** + * Get the serial nr of the Qbus server. + * + * @return the sn + */ + public String getSn() { + Configuration config = this.getConfig(); + return ((String) config.get(CONFIG_SN)); + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java new file mode 100644 index 0000000000000..5a1cb996c4a91 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java @@ -0,0 +1,69 @@ +/** + * 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.qbus.internal; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; + +import org.openhab.binding.qbus.internal.handler.QbusBistabielHandler; +import org.openhab.binding.qbus.internal.handler.QbusCO2Handler; +import org.openhab.binding.qbus.internal.handler.QbusDimmerHandler; +import org.openhab.binding.qbus.internal.handler.QbusRolHandler; +import org.openhab.binding.qbus.internal.handler.QbusSceneHandler; +import org.openhab.binding.qbus.internal.handler.QbusThermostatHandler; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingTypeUID; +import org.openhab.core.thing.binding.BaseThingHandlerFactory; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerFactory; +import org.osgi.service.component.annotations.Component; + +/** + * The {@link qbusHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Koen Schockaert - Initial Contribution + */ + +@Component(service = ThingHandlerFactory.class, configurationPid = "binding.qbus") +public class QbusHandlerFactory extends BaseThingHandlerFactory { + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID) || BRIDGE_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected ThingHandler createHandler(Thing thing) { + if (BRIDGE_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + QbusBridgeHandler handler = new QbusBridgeHandler((Bridge) thing); + return handler; + } else if (SCENE_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusSceneHandler(thing); + } else if (BISTABIEL_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusBistabielHandler(thing); + } else if (THERMOSTAT_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusThermostatHandler(thing); + } else if (DIMMER_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusDimmerHandler(thing); + } else if (CO2_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusCO2Handler(thing); + } else if (ROLLERSHUTTER_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusRolHandler(thing); + } else if (ROLLERSHUTTER_SLATS_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { + return new QbusRolHandler(thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java new file mode 100644 index 0000000000000..fb45b86151077 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java @@ -0,0 +1,183 @@ +/** + * 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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusBistabiel; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusBistabielHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public class QbusBistabielHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusBistabielHandler.class); + + // private volatile int prevBistabielState; + + public QbusBistabielHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + Integer BistabielId = ((Number) this.getConfig().get(CONFIG_BISTABIEL_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute bistabiel " + BistabielId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute bistabiel " + BistabielId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: bridge communication not initialized when trying to execute bistabiel " + BistabielId); + return; + } + + QbusBistabiel QBistabiel = QComm.getBistabiel().get(BistabielId); + + if (QComm.communicationActive()) { + handleCommandSelection(QBistabiel, channelUID, command); + } else { + // We lost connection but the connection object is there, so was correctly started. + // Try to restart communication. + // This can be expensive, therefore do it in a job. + scheduler.submit(() -> { + QComm.restartCommunication(); + // If still not active, take thing offline and return. + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + // Also put the bridge back online + QBridgeHandler.bridgeOnline(); + + // And finally handle the command + handleCommandSelection(QBistabiel, channelUID, command); + }); + } + } + + private void handleCommandSelection(QbusBistabiel QBistabiel, ChannelUID channelUID, Command command) { + logger.debug("Qbus: handle command {} for {}", command, channelUID); + + if (command == REFRESH) { + handleStateUpdate(QBistabiel); + return; + } + + handleSwitchCommand(QBistabiel, command); + updateStatus(ThingStatus.ONLINE); + } + + private void handleSwitchCommand(QbusBistabiel QBistabiel, Command command) { + if (command instanceof OnOffType) { + OnOffType s = (OnOffType) command; + + @SuppressWarnings("null") + String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + + if (s == OnOffType.OFF) { + QBistabiel.execute(0, sn); + } else { + QBistabiel.execute(100, sn); + } + } + } + + @Override + public void initialize() { + Configuration config = this.getConfig(); + + Integer BistabielId = ((Number) config.get(CONFIG_BISTABIEL_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for bistabiel " + BistabielId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for bistabiel " + BistabielId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + if (QComm == null || !QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: no connection with Qbus server, could not initialize bistabiel " + BistabielId); + return; + } + + QbusBistabiel QBistabiel = QComm.getBistabiel().get(BistabielId); + + // int bistabielState = QBistabiel.getState(); + + // this.prevBistabielState = bistabielState; + QBistabiel.setThingHandler(this); + + Map properties = new HashMap<>(); + + thing.setProperties(properties); + + handleStateUpdate(QBistabiel); + + logger.debug("Qbus: bistabiel intialized {}", BistabielId); + } + + /** + * Method to update state of channel, called from Qbus Bistabiel. + */ + public void handleStateUpdate(QbusBistabiel QBistabiel) { + + int bistabielState = QBistabiel.getState(); + + updateState(CHANNEL_SWITCH, (bistabielState == 0) ? OnOffType.OFF : OnOffType.ON); + updateStatus(ThingStatus.ONLINE); + + // this.prevBistabielState = bistabielState; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java new file mode 100644 index 0000000000000..b500901e62073 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java @@ -0,0 +1,167 @@ +/** + * 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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusCO2; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusCO2Handler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public class QbusCO2Handler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusCO2Handler.class); + + // private volatile int prevCO2State; + + public QbusCO2Handler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + Integer CO2Id = ((Number) this.getConfig().get(CONFIG_CO2_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute CO2 " + CO2Id); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute CO2 " + CO2Id); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: bridge communication not initialized when trying to execute CO2 " + CO2Id); + return; + } + + QbusCO2 QCO2 = QComm.getCo2().get(CO2Id); + + if (QComm.communicationActive()) { + handleCommandSelection(QCO2, channelUID, command); + } else { + // We lost connection but the connection object is there, so was correctly started. + // Try to restart communication. + // This can be expensive, therefore do it in a job. + scheduler.submit(() -> { + QComm.restartCommunication(); + // If still not active, take thing offline and return. + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + // Also put the bridge back online + QBridgeHandler.bridgeOnline(); + + // And finally handle the command + handleCommandSelection(QCO2, channelUID, command); + }); + } + } + + private void handleCommandSelection(QbusCO2 QCO2, ChannelUID channelUID, Command command) { + logger.debug("Qbus: handle command {} for {}", command, channelUID); + + if (command == REFRESH) { + handleStateUpdate(QCO2); + return; + } + + updateStatus(ThingStatus.ONLINE); + } + + @Override + public void initialize() { + Configuration config = this.getConfig(); + + Integer CO2Id = ((Number) config.get(CONFIG_CO2_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for CO2 " + CO2Id); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for CO2 " + CO2Id); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + if (QComm == null || !QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: no connection with Qbus server, could not initialize CO2 " + CO2Id); + return; + } + + QbusCO2 QCO2 = QComm.getCo2().get(CO2Id); + + // int actionState = QCO2.getState(); + + // this.prevCO2State = actionState; + QCO2.setThingHandler(this); + + Map properties = new HashMap<>(); + + thing.setProperties(properties); + + handleStateUpdate(QCO2); + + logger.debug("Qbus: CO2 intialized {}", CO2Id); + } + + /** + * Method to update state of channel, called from Qbus CO2. + */ + public void handleStateUpdate(QbusCO2 QCO2) { + + int CO2State = QCO2.getState(); + + updateState(CHANNEL_CO2, new DecimalType(CO2State)); + updateStatus(ThingStatus.ONLINE); + + // this.prevCO2State = CO2State; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java new file mode 100644 index 0000000000000..0ff3d2412fa00 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java @@ -0,0 +1,250 @@ +/** + * 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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.binding.qbus.internal.protocol.QbusDimmer; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusDimmerHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public class QbusDimmerHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusDimmerHandler.class); + + // private volatile int prevDimmerState; + + public QbusDimmerHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + Integer dimmerId = ((Number) this.getConfig().get(CONFIG_DIMMER_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute dimmer " + dimmerId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute dimmer " + dimmerId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: bridge communication not initialized when trying to execute dimmer " + dimmerId); + return; + } + + QbusDimmer QDimmer = QComm.getDimmer().get(dimmerId); + + /* + * if (QDimmer == null) { + * updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + * "Qbus: dimmerId " + dimmerId + " does not match a dimmer in the controller"); + * return; + * } + */ + if (QComm.communicationActive()) { + handleCommandSelection(QDimmer, channelUID, command); + } else { + // We lost connection but the connection object is there, so was correctly started. + // Try to restart communication. + // This can be expensive, therefore do it in a job. + scheduler.submit(() -> { + QComm.restartCommunication(); + // If still not active, take thing offline and return. + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + // Also put the bridge back online + QBridgeHandler.bridgeOnline(); + + // And finally handle the command + handleCommandSelection(QDimmer, channelUID, command); + }); + } + } + + private void handleCommandSelection(QbusDimmer QDimmer, ChannelUID channelUID, Command command) { + logger.debug("Qbus: handle command {} for {}", command, channelUID); + + if (command == REFRESH) { + handleStateUpdate(QDimmer); + return; + } + + switch (channelUID.getId()) { + case CHANNEL_SWITCH: + handleSwitchCommand(QDimmer, command); + updateStatus(ThingStatus.ONLINE); + break; + + case CHANNEL_BRIGHTNESS: + handleBrightnessCommand(QDimmer, command); + updateStatus(ThingStatus.ONLINE); + break; + + default: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: channel unknown " + channelUID.getId()); + } + } + + private void handleSwitchCommand(QbusDimmer QDimmer, Command command) { + + @SuppressWarnings("null") + String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + + if (command instanceof OnOffType) { + OnOffType s = (OnOffType) command; + if (s == OnOffType.OFF) { + QDimmer.execute(0, sn); + } else { + QDimmer.execute(100, sn); + } + } + } + + private void handleBrightnessCommand(QbusDimmer QDimmer, Command command) { + // Bridge QBridge = getBridge(); + @SuppressWarnings("null") + String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + + if (command instanceof OnOffType) { + OnOffType s = (OnOffType) command; + if (s == OnOffType.OFF) { + QDimmer.execute(0, sn); + } else { + QDimmer.execute(100, sn); + } + } else if (command instanceof IncreaseDecreaseType) { + IncreaseDecreaseType s = (IncreaseDecreaseType) command; + int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); + int currentValue = QDimmer.getState(); + int newValue; + if (s == IncreaseDecreaseType.INCREASE) { + newValue = currentValue + stepValue; + // round down to step multiple + newValue = newValue - newValue % stepValue; + QDimmer.execute(newValue > 100 ? 100 : newValue, sn); + } else { + newValue = currentValue - stepValue; + // round up to step multiple + newValue = newValue + newValue % stepValue; + QDimmer.execute(newValue < 0 ? 0 : newValue, sn); + } + } else if (command instanceof PercentType) { + PercentType p = (PercentType) command; + if (p == PercentType.ZERO) { + QDimmer.execute(0, sn); + } else { + QDimmer.execute(p.intValue(), sn); + } + } + } + + @Override + public void initialize() { + Configuration config = this.getConfig(); + + Integer dimmerId = ((Number) config.get(CONFIG_DIMMER_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for dimmer " + dimmerId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for dimmer " + dimmerId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + if (QComm == null || !QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: no connection with Qbus, could not initialize dimmer " + dimmerId); + return; + } + + QbusDimmer QDimmer = QComm.getDimmer().get(dimmerId); + /* + * if (QDimmer == null) { + * updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + * "Qbus: dimmerId does not match an action in the server " + dimmerId); + * return; + * } + */ + // int actionState = QDimmer.getState(); + + // this.prevDimmerState = actionState; + QDimmer.setThingHandler(this); + + Map properties = new HashMap<>(); + + thing.setProperties(properties); + + handleStateUpdate(QDimmer); + + logger.debug("Qbus: dimmer intialized {}", dimmerId); + } + + /** + * Method to update state of channel, called from Qbus Dimmer. + */ + public void handleStateUpdate(QbusDimmer QDimmer) { + + int dimmerState = QDimmer.getState(); + + updateState(CHANNEL_BRIGHTNESS, new PercentType(dimmerState)); + + updateStatus(ThingStatus.ONLINE); + + // this.prevDimmerState = dimmerState; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java new file mode 100644 index 0000000000000..fa9bad35e3458 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.binding.qbus.internal.protocol.QbusRol; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.IncreaseDecreaseType; +import org.openhab.core.library.types.PercentType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusRolHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public class QbusRolHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusRolHandler.class); + + // private volatile int prevRolState; + // private volatile int prevSlatState; + + public QbusRolHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + Integer RolId = ((Number) this.getConfig().get(CONFIG_ROLLERSHUTTER_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute Slats " + RolId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute Slats " + RolId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: bridge communication not initialized when trying to execute Slats " + RolId); + return; + } + + QbusRol QRol = QComm.getRol().get(RolId); + + if (QComm.communicationActive()) { + handleCommandSelection(QRol, channelUID, command); + } else { + // We lost connection but the connection object is there, so was correctly started. + // Try to restart communication. + // This can be expensive, therefore do it in a job. + scheduler.submit(() -> { + QComm.restartCommunication(); + // If still not active, take thing offline and return. + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + // Also put the bridge back online + QBridgeHandler.bridgeOnline(); + + // And finally handle the command + handleCommandSelection(QRol, channelUID, command); + }); + } + } + + private void handleCommandSelection(QbusRol qRol, ChannelUID channelUID, Command command) { + logger.debug("Qbus: handle command {} for {}", command, channelUID); + + if (command == REFRESH) { + handleStateUpdate(qRol); + return; + } + + switch (channelUID.getId()) { + case CHANNEL_ROLLERSHUTTER: + handleBrightnessCommand(qRol, command); + updateStatus(ThingStatus.ONLINE); + break; + + case CHANNEL_SLATS: + handleSlatsCommand(qRol, command); + updateStatus(ThingStatus.ONLINE); + break; + + default: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: channel unknown " + channelUID.getId()); + } + } + + private void handleBrightnessCommand(QbusRol QRol, Command command) { + @SuppressWarnings("null") + String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + + if (command instanceof org.openhab.core.library.types.UpDownType) { + org.openhab.core.library.types.UpDownType s = (org.openhab.core.library.types.UpDownType) command; + if (s == org.openhab.core.library.types.UpDownType.DOWN) { + QRol.execute(0, sn); + } else { + QRol.execute(100, sn); + } + } else if (command instanceof IncreaseDecreaseType) { + IncreaseDecreaseType s = (IncreaseDecreaseType) command; + int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); + int currentValue = QRol.getState(); + int newValue; + if (s == IncreaseDecreaseType.INCREASE) { + newValue = currentValue + stepValue; + // round down to step multiple + newValue = newValue - newValue % stepValue; + QRol.execute(newValue > 100 ? 100 : newValue, sn); + } else { + newValue = currentValue - stepValue; + // round up to step multiple + newValue = newValue + newValue % stepValue; + QRol.execute(newValue < 0 ? 0 : newValue, sn); + } + } else if (command instanceof PercentType) { + PercentType p = (PercentType) command; + if (p == PercentType.ZERO) { + QRol.execute(0, sn); + } else { + QRol.execute(p.intValue(), sn); + } + } + } + + private void handleSlatsCommand(QbusRol QRol, Command command) { + @SuppressWarnings("null") + String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + + if (command instanceof org.openhab.core.library.types.UpDownType) { + org.openhab.core.library.types.UpDownType s = (org.openhab.core.library.types.UpDownType) command; + if (s == org.openhab.core.library.types.UpDownType.DOWN) { + QRol.executeSlats(0, sn); + } else { + QRol.executeSlats(100, sn); + } + } else if (command instanceof IncreaseDecreaseType) { + IncreaseDecreaseType s = (IncreaseDecreaseType) command; + int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); + int currentValue = QRol.getState(); + int newValue; + if (s == IncreaseDecreaseType.INCREASE) { + newValue = currentValue + stepValue; + // round down to step multiple + newValue = newValue - newValue % stepValue; + QRol.executeSlats(newValue > 100 ? 100 : newValue, sn); + } else { + newValue = currentValue - stepValue; + // round up to step multiple + newValue = newValue + newValue % stepValue; + QRol.executeSlats(newValue < 0 ? 0 : newValue, sn); + } + } else if (command instanceof PercentType) { + PercentType p = (PercentType) command; + if (p == PercentType.ZERO) { + QRol.executeSlats(0, sn); + } else { + QRol.executeSlats(p.intValue(), sn); + } + } + } + + @Override + public void initialize() { + Configuration config = this.getConfig(); + + Integer RolId = ((Number) config.get(CONFIG_ROLLERSHUTTER_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for Slats " + RolId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for Slats " + RolId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + if (QComm == null || !QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: no connection with Qbus server, could not initialize Slats " + RolId); + return; + } + + QbusRol QRol = QComm.getRol().get(RolId); + /* + * int rolState = QRol.getState(); + * int slatState = QRol.getStateSlats(); + * + * this.prevRolState = rolState; + * this.prevSlatState = slatState; + * + */ + QRol.setThingHandler(this); + + Map properties = new HashMap<>(); + + thing.setProperties(properties); + + handleStateUpdate(QRol); + + logger.debug("Qbus: Slats intialized {}", RolId); + } + + /** + * Method to update state of channel, called from Qbus Slats. + */ + public void handleStateUpdate(QbusRol qRol) { + + int rolState = qRol.getState().intValue(); + int slatState = qRol.getStateSlats().intValue(); + + updateState(CHANNEL_ROLLERSHUTTER, new PercentType(rolState)); + updateState(CHANNEL_SLATS, new PercentType(slatState)); + updateStatus(ThingStatus.ONLINE); + + // this.prevRolState = rolState; + // this.prevSlatState = slatState; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java new file mode 100644 index 0000000000000..ca810f3bd4fe7 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java @@ -0,0 +1,181 @@ +/** + * 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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.types.RefreshType.REFRESH; + +import java.util.HashMap; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.binding.qbus.internal.protocol.QbusScene; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusSceneHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public class QbusSceneHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusSceneHandler.class); + + // private volatile int prevSceneState; + + public QbusSceneHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + Integer SceneId = ((Number) this.getConfig().get(CONFIG_SCENE_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute Scene " + SceneId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute Scene " + SceneId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: bridge communication not initialized when trying to execute scene " + SceneId); + return; + } + + QbusScene QScene = QComm.getScenes().get(SceneId); + + if (QComm.communicationActive()) { + handleCommandSelection(QScene, channelUID, command); + } else { + // We lost connection but the connection object is there, so was correctly started. + // Try to restart communication. + // This can be expensive, therefore do it in a job. + scheduler.submit(() -> { + QComm.restartCommunication(); + // If still not active, take thing offline and return. + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + // Also put the bridge back online + QBridgeHandler.bridgeOnline(); + + // And finally handle the command + handleCommandSelection(QScene, channelUID, command); + }); + } + } + + private void handleCommandSelection(QbusScene QScene, ChannelUID channelUID, Command command) { + logger.debug("Qbus: handle command {} for {}", command, channelUID); + + if (command == REFRESH) { + handleStateUpdate(QScene); + return; + } + + handleSwitchCommand(QScene, command); + updateStatus(ThingStatus.ONLINE); + } + + private void handleSwitchCommand(QbusScene QScene, Command command) { + @SuppressWarnings("null") + String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + if (command instanceof OnOffType) { + OnOffType s = (OnOffType) command; + if (s == OnOffType.OFF) { + QScene.execute(0, sn); + } else { + QScene.execute(100, sn); + } + } + } + + @Override + public void initialize() { + Configuration config = this.getConfig(); + + Integer SceneId = ((Number) config.get(CONFIG_SCENE_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for scene " + SceneId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for scene " + SceneId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + if (QComm == null || !QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: no connection with Qbus server, could not initialize scene " + SceneId); + return; + } + + QbusScene QScene = QComm.getScenes().get(SceneId); + + // int sceneState = QScene.getState(); + /* + * this.prevSceneState = sceneState; + * QScene.setThingHandler(this); + */ + Map properties = new HashMap<>(); + + thing.setProperties(properties); + + handleStateUpdate(QScene); + + logger.debug("Qbus: scene intialized {}", SceneId); + } + + /** + * Method to update state of channel, called from Qbus Scene. + */ + public void handleStateUpdate(QbusScene QScene) { + + int sceneState = QScene.getState(); + + updateState(CHANNEL_SWITCH, (sceneState == 0) ? OnOffType.OFF : OnOffType.ON); + updateStatus(ThingStatus.ONLINE); + + // this.prevSceneState = sceneState; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java new file mode 100644 index 0000000000000..3120718e73e01 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java @@ -0,0 +1,183 @@ +/** + * 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.qbus.internal.handler; + +import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.library.unit.SIUnits.CELSIUS; +import static org.openhab.core.types.RefreshType.REFRESH; + +import javax.measure.quantity.Temperature; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QThermostat; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.core.config.core.Configuration; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusThermostatHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public class QbusThermostatHandler extends BaseThingHandler { + + private final Logger logger = LoggerFactory.getLogger(QbusThermostatHandler.class); + + public QbusThermostatHandler(Thing thing) { + super(thing); + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + Integer thermostatId = ((Number) this.getConfig().get(CONFIG_THERMOSTAT_ID)).intValue(); + + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute thermostat command " + thermostatId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized when trying to execute thermostat command " + thermostatId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: bridge communication not initialized when trying to execute thermostat command " + + thermostatId); + return; + } + + QThermostat qThermostat = QComm.getThermostats().get(thermostatId); + + if (QComm.communicationActive()) { + handleCommandSelection(qThermostat, channelUID, command); + } else { + scheduler.submit(() -> { + QComm.restartCommunication(); + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + QBridgeHandler.bridgeOnline(); + handleCommandSelection(qThermostat, channelUID, command); + }); + } + } + + @SuppressWarnings("unchecked") + private void handleCommandSelection(QThermostat qThermostat, ChannelUID channelUID, Command command) { + logger.debug("Qbus: handle command {} for {}", command, channelUID); + @SuppressWarnings("null") + String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + + if (REFRESH.equals(command)) { + handleStateUpdate(qThermostat); + return; + } + + switch (channelUID.getId()) { + case CHANNEL_MEASURED: + updateStatus(ThingStatus.ONLINE); + break; + + case CHANNEL_MODE: + if (command instanceof DecimalType) { + qThermostat.executeMode(((DecimalType) command).intValue(), sn); + } + updateStatus(ThingStatus.ONLINE); + break; + + case CHANNEL_SETPOINT: + if (command instanceof QuantityType) { + qThermostat.executeSetpoint(((QuantityType) command).doubleValue(), sn); + } + updateStatus(ThingStatus.ONLINE); + + break; + + default: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: channel unknown " + channelUID.getId()); + } + } + + @Override + public void initialize() { + Configuration config = this.getConfig(); + + Integer thermostatId = ((Number) config.get(CONFIG_THERMOSTAT_ID)).intValue(); + + Bridge nhcBridge = getBridge(); + if (nhcBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for thermostat " + thermostatId); + return; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) nhcBridge.getHandler(); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "Qbus: no bridge initialized for thermostat " + thermostatId); + return; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + if (QComm == null || !QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: no connection with Qbus server, could not initialize thermostat " + thermostatId); + return; + } + + QThermostat qThermostat = QComm.getThermostats().get(thermostatId); + + qThermostat.setThingHandler(this); + + handleStateUpdate(qThermostat); + + logger.debug("Qbus: thermostat intialized {}", thermostatId); + } + + /** + * Method to update state of all channels, called from Qbus thermostat. + * + * @param qThermostat Qbus thermostat + * + */ + public void handleStateUpdate(QThermostat qThermostat) { + + updateState(CHANNEL_MEASURED, new QuantityType(qThermostat.getMeasured(), CELSIUS)); + + updateState(CHANNEL_SETPOINT, new QuantityType(qThermostat.getSetpoint(), CELSIUS)); + + updateState(CHANNEL_MODE, new DecimalType(qThermostat.getMode())); + + updateStatus(ThingStatus.ONLINE); + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java new file mode 100644 index 0000000000000..78b89d7543ef5 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java @@ -0,0 +1,75 @@ +/** + * 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.qbus.internal.protocol; + +/** + * Class {@link QbusMessageCmd} used as input to gson to send commands to Qbus. Extends + * {@link QbusMessageBase}. + *

+ * Example: {"cmd":"executebistabiel","id":1,"value1":0} + * + * @author Koen Schockaert - Initial Contribution + */ + +@SuppressWarnings("unused") +class QMessageCmd extends QbusMessageBase { + + private int id; + private Integer pos; + private Integer value1; + private Integer value2; + private Integer value3; + private Integer mode; + private Double setpoint; + private String ctdsn; + + QMessageCmd(String cmd) { + super.setCmd(cmd); + } + + QMessageCmd(String cmd, int id) { + this(cmd); + this.id = id; + } + + QMessageCmd(String cmd, int id, Integer value1) { + this(cmd, id); + this.value1 = value1; + } + + QMessageCmd(String cmd, int id, Integer value1, Integer value2) { + this(cmd, id, value1); + this.value2 = value2; + } + + QMessageCmd(String cmd, int id, Integer value1, Integer value2, Integer value3) { + this(cmd, id, value1); + this.value2 = value2; + this.value3 = value3; + } + + QMessageCmd withMode(Integer mode) { + this.mode = mode; + return this; + } + + QMessageCmd withSetpoint(Double d) { + this.setpoint = d; + return this; + } + + QMessageCmd withSn(String sn) { + this.ctdsn = sn; + return this; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageListMap.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageListMap.java new file mode 100644 index 0000000000000..1bc73610537a0 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageListMap.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.qbus.internal.protocol; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * Class {@link QbusMessageListMap} used as output from gson for cmd or event feedback from Qbus where the + * data part is enclosed by [] and contains a list of json strings. Extends {@link QbusMessageBase}. + *

+ * + * @author Koen Schockaert - Initial Contribution + */ + +class QMessageListMap extends QbusMessageBase { + + private List> data = new ArrayList<>(); + + List> getData() { + return this.data; + } + + void setData(List> data) { + this.data = data; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageMap.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageMap.java new file mode 100644 index 0000000000000..139ecee65bd47 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageMap.java @@ -0,0 +1,36 @@ +/** + * 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.qbus.internal.protocol; + +import java.util.HashMap; +import java.util.Map; + +/** + * Class {@link QbusMessageMap} used as output from gson for cmd or event feedback from Qbus where the + * data part is a simple json string. Extends {@link QbusMessageBase}. + *

+ * + * @author Koen Schockaert - Initial Contribution + */ +class QMessageMap extends QbusMessageBase { + + private Map data = new HashMap<>(); + + Map getData() { + return this.data; + } + + void setData(Map data) { + this.data = data; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QThermostat.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QThermostat.java new file mode 100644 index 0000000000000..4663194c57fa8 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QThermostat.java @@ -0,0 +1,175 @@ +/** + * 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.qbus.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusThermostatHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QThermostat} class represents the thermostat Qbus communication object. It contains all + * fields representing a Qbus thermostat and has methods to set the thermostat mode and setpoint in Qbus and + * receive thermostat updates. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public final class QThermostat { + + private final Logger logger = LoggerFactory.getLogger(QThermostat.class); + + @Nullable + private QbusCommunication qComm; + + private int id; + private Double measured = 0.0; + private Double setpoint = 0.0; + private Integer mode = 0; + + @Nullable + private QbusThermostatHandler thingHandler; + + QThermostat(int id) { + this.id = id; + } + + /** + * Update all values of the thermostat + * + * @param measured current temperature in 1°C multiples + * @param setpoint the setpoint temperature in 1°C multiples + * @param mode 0="Manueel", 1="Vorst", 2="Economisch", 3="Comfort", 4="Nacht" + */ + public void updateState(Double measured, Double setpoint, Integer mode) { + setMeasured(measured); + setSetpoint(setpoint); + setMode(mode); + + QbusThermostatHandler handler = thingHandler; + if (handler != null) { + logger.debug("Qbus: update channels for {}", id); + handler.handleStateUpdate(this); + } + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to the termostat is initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the thermostat receives an update from the Qbus IP-interface. + * + * @param handler + */ + public void setThingHandler(QbusThermostatHandler handler) { + this.thingHandler = handler; + } + + /** + * This method sets a pointer to the qComm object of class {@link QbusCommuncation}. + * This is then used to be able to call back the sendCommand method in this class to send a command to the + * Qbus IP-interface when.. + * + * @param qComm + */ + public void setQComm(QbusCommunication qComm) { + this.qComm = qComm; + } + + /** + * Get measured temperature. + * + * @return measured temperature in 0.5°C multiples + */ + public double getMeasured() { + return this.measured; + } + + /** + * Set measured temperature. + * + * @param measured + */ + private void setMeasured(Double measured) { + this.measured = measured; + } + + /** + * Get setpoint + * + * @return the setpoint temperature in 1°C multiples + */ + public double getSetpoint() { + return this.setpoint; + } + + /** + * Set setpoint temperature. + * + * @param setpoint + */ + private void setSetpoint(Double setpoint) { + this.setpoint = setpoint; + } + + /** + * Get the thermostat mode. + * + * @return the mode: 0="Manueel", 1="Vorst", 2="Economisch", 3="Comfort", 4="Nacht" + */ + public Integer getMode() { + return mode; + } + + /** + * Set the thermostat + * + * mode: 0="Manueel", 1="Vorst", 2="Economisch", 3="Comfort", 4="Nacht" + */ + private void setMode(Integer mode) { + this.mode = mode; + } + + /** + * Sends thermostat mode to Qbus. + * + * @param mode + * @param sn + */ + public void executeMode(int mode, String sn) { + logger.debug("Qbus: execute thermostat mode {} for {} on {}", mode, this.id, sn); + + QMessageCmd qCmd = new QMessageCmd("executethermostat", this.id).withMode(mode).withSn(sn); + + QbusCommunication comm = qComm; + if (comm != null) { + comm.sendMessage(qCmd); + } + } + + /** + * Sends setpoint to Qbus. + * + * @param d + */ + public void executeSetpoint(double d, String sn) { + logger.debug("Qbus: execute thermostat setpoint {} for {} on {}", d, this.id, sn); + + QMessageCmd qCmd = new QMessageCmd("executethermostat", this.id).withSetpoint(d).withSn(sn); + + QbusCommunication comm = qComm; + if (comm != null) { + comm.sendMessage(qCmd); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java new file mode 100644 index 0000000000000..52547826ee8f6 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java @@ -0,0 +1,103 @@ +/** + * 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.qbus.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusBistabielHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusBistabiel} class represents the Qbus BISTABIEL output. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public final class QbusBistabiel { + + private final Logger logger = LoggerFactory.getLogger(QbusBistabiel.class); + + @Nullable + private QbusCommunication QComm; + + private int id; + private Integer state = 0; + + @Nullable + private QbusBistabielHandler thingHandler; + + QbusBistabiel(int id) { + this.id = id; + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to this bistabiel is initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the bistable output receives an update from the Qbus IP-interface. + * + * @param handler + */ + public void setThingHandler(QbusBistabielHandler handler) { + this.thingHandler = handler; + } + + /** + * This method sets a pointer to the QComm BISTABIEL of class {@link QbusCommuncation}. + * This is then used to be able to call back the sendCommand method in this class to send a command to the + * Qbus IP-interface when.. + * + * @param QComm + */ + public void setQComm(QbusCommunication QComm) { + this.QComm = QComm; + } + + /** + * Get state of bistabiel. + * + * @return bistabiel state + */ + public Integer getState() { + return this.state; + } + + /** + * Sets state of bistabiel. + * + * @param bistabiel state + */ + void setState(int state) { + this.state = state; + QbusBistabielHandler handler = thingHandler; + if (handler != null) { + logger.debug("Qbus: update bistabiel channel state for {} with {}", id, state); + handler.handleStateUpdate(this); + } + } + + /** + * Sends bistabiel to Qbus. + */ + public void execute(int value, String sn) { + + logger.debug("Qbus: execute bistabiel for {} on CTD {}", this.id, sn); + + QMessageCmd QCmd = new QMessageCmd("executebistabiel", this.id, value).withSn(sn); + + QbusCommunication comm = QComm; + if (comm != null) { + comm.sendMessage(QCmd); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java new file mode 100644 index 0000000000000..b1aa025c16680 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java @@ -0,0 +1,75 @@ +/** + * 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.qbus.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusCO2Handler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusCO2} class represents the action Qbus CO2 output. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public final class QbusCO2 { + + private final Logger logger = LoggerFactory.getLogger(QbusCO2.class); + + private int id; + private Integer state = 0; + + @Nullable + private QbusCO2Handler thingHandler; + + QbusCO2(int id) { + this.id = id; + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to this CO2 is initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the CO2 output receives an update from the Qbus IP-interface. + * + * @param handler + */ + public void setThingHandler(QbusCO2Handler handler) { + this.thingHandler = handler; + } + + /** + * Get state of CO2. + * + * @return CO2 state + */ + public Integer getState() { + return this.state; + } + + /** + * Sets state of CO2. + * + * @param CO2 state + */ + public void setState(Integer co2) { + this.state = co2; + QbusCO2Handler handler = thingHandler; + if (handler != null) { + logger.debug("Qbus: update channel state for {} with {}", id, state); + handler.handleStateUpdate(this); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java new file mode 100644 index 0000000000000..47c2a115ffedc --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java @@ -0,0 +1,936 @@ +/** + * 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.qbus.internal.protocol; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.Socket; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonParseException; + +/** + * The {@link QbusCommunication} class is able to do the following tasks with Qbus + * systems: + *

    + *
  • Start and stop TCP socket connection with Qbus Server. + *
  • Read all setup and status information from the Qbus Controller. + *
  • Execute Qbus commands. + *
  • Listen to events from Qbus. + *
+ * + * A class instance is instantiated from the {@link QbusBridgeHandler} class initialization. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public final class QbusCommunication { + + private final Logger logger = LoggerFactory.getLogger(QbusCommunication.class); + + @Nullable + private Socket qSocket; + @Nullable + private PrintWriter qOut; + @Nullable + private BufferedReader qIn; + + private boolean listenerStopped; + private boolean qEventsRunning; + + private Gson gsonOut = new Gson(); + private Gson gsonIn; + + private final Map scenes = new HashMap<>(); + private final Map bistabiel = new HashMap<>(); + private final Map dimmer = new HashMap<>(); + private final Map thermostats = new HashMap<>(); + private final Map co2 = new HashMap<>(); + private final Map Rol = new HashMap<>(); + // private final Map Disconnect = new HashMap<>(); + + @Nullable + private QbusBridgeHandler bridgeCallBack; + + /** + * Constructor for Qbus communication object, manages communication with + * Qbus Server. + * + */ + public QbusCommunication() { + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(QbusMessageBase.class, new QbusMessageDeserializer()); + this.gsonIn = gsonBuilder.create(); + } + + /** + * Start communication with Qbus Server, run through initialization and start thread listening + * to all messages coming from Qbus. + * + * @param addr : IP-address of Qbus Server + * @param port : Communication port of Qbus server + * @param sn : Serial number of the controller + * + */ + public synchronized void startCommunication() { + QbusBridgeHandler handler = this.bridgeCallBack; + + try { + for (int i = 1; qEventsRunning && (i <= 5); i++) { + Thread.sleep(1000); + } + if (qEventsRunning) { + logger.error("Qbus: starting from thread {}, but previous connection still active after 5000ms", + Thread.currentThread().getId()); + throw new IOException(); + } + + if (handler == null) { + throw new IOException(); + } + + InetAddress addr = handler.getAddr(); + int port = handler.getPort(); + + Socket socket = new Socket(addr, port); + this.qSocket = socket; + this.qOut = new PrintWriter(socket.getOutputStream(), true); + this.qIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); + logger.debug("Qbus: connected via local port {} from thread {}", socket.getLocalPort(), + Thread.currentThread().getId()); + + initialize(); + + (new Thread(qEvents)).start(); + + } catch (IOException | InterruptedException e) { + logger.warn("Qbus: error initializing communication from thread {}", Thread.currentThread().getId()); + if (handler != null) { + handler.bridgeOffline(); + } + stopCommunication(); + + } + } + + /** + * Cleanup socket when the communication with Qbus Server is closed. + * + * @throws IOException + * + */ + public synchronized void stopCommunication() { + this.listenerStopped = true; + + Socket socket = this.qSocket; + + if (socket != null) { + try { + socket.close(); + } catch (IOException ignore) { + // ignore IO Error when trying to close the socket if the intention is to close it anyway + } + } + + this.qSocket = null; + // restartCommunication(); + logger.debug("Qbus: communication stopped from thread {}", Thread.currentThread().getId()); + } + + /** + * Close and restart communication with Qbus Server. + * + */ + public synchronized void restartCommunication() { + QbusBridgeHandler handler = this.bridgeCallBack; + stopCommunication(); + + // handler.bridgeOffline(); + + if (handler != null) { + handler.bridgeOffline(); + } + + logger.debug("Qbus: restart communication from thread {}", Thread.currentThread().getId()); + + startCommunication(); + } + + /** + * Method to check if communication with Qbus Server is active + * + * @return True if active + */ + public boolean communicationActive() { + return (this.qSocket != null); + } + + /** + * Runnable that handles inbound communication from Qbus server. + *

+ * The thread listens to the TCP socket opened at instantiation of the {@link QbusCommunication} class + * and interprets all inbound json messages. It triggers state updates for active channels linked to the + * Qbus outputs. It is started after initialization of the communication. + * + */ + private Runnable qEvents = () -> { + String qMessage; + // QbusBridgeHandler handler = this.bridgeCallBack; + + logger.debug("Qbus: listening for events on thread {}", Thread.currentThread().getId()); + listenerStopped = false; + qEventsRunning = true; + + BufferedReader reader = this.qIn; + + try { + if (reader == null) { + throw new IOException(); + } + while (!listenerStopped & ((qMessage = reader.readLine()) != null)) { + if (qMessage != null) { + readMessage(qMessage); + } + } + } catch (IOException e) { + if (!listenerStopped) { + qEventsRunning = false; + logger.warn("Qbus: IO error in listener on thread {}", Thread.currentThread().getId()); + restartCommunication(); + return; + } + } + + qEventsRunning = false; + + stopCommunication(); + + logger.debug("Qbus: event listener thread stopped on thread {}", Thread.currentThread().getId()); + + }; + + /** + * Method that interprets all feedback from Qbus Server application and calls appropriate handling methods. + * + * @param qMessage message read from Qbus. + */ + private void readMessage(String qMessage) { + logger.debug("Qbus: received json on thread {}", Thread.currentThread().getId()); + + try { + QbusMessageBase qMessageGson = this.gsonIn.fromJson(qMessage, QbusMessageBase.class); + String cmd = ""; + String event = ""; + + @SuppressWarnings("null") + String confsn = this.bridgeCallBack.getSn(); + String sn = qMessageGson.getSn(); + cmd = qMessageGson.getCmd(); + event = qMessageGson.getEvent(); + + // logger.debug(cmd); + // logger.debug(event); + + if (Integer.parseInt(confsn) == Integer.parseInt(sn)) { + // Get the compatible outputs from the Qbus server + if ("listbistabiel".equals(cmd)) { + cmdListBistabiel(((QMessageListMap) qMessageGson).getData()); + } else if ("listdimmers".equals(cmd)) { + cmdListDimmers(((QMessageListMap) qMessageGson).getData()); + } else if (("listthermostats").equals(cmd)) { + cmdListThermostat(((QMessageListMap) qMessageGson).getData()); + } else if (("listscenes").equals(cmd)) { + cmdlistscenes(((QMessageListMap) qMessageGson).getData()); + } else if (("listco2").equals(cmd)) { + cmdlistco2(((QMessageListMap) qMessageGson).getData()); + } else if (("listrol").equals(cmd)) { + cmdlistrol(((QMessageListMap) qMessageGson).getData()); + } else if (("listrol02pslats").equals(cmd)) { + cmdlistrolslats(((QMessageListMap) qMessageGson).getData()); + } + // Commands to execute from openHAB to Qbus + else if ("executebistabiel".equals(cmd)) { + cmdExecuteBistabiel(((QMessageMap) qMessageGson).getData()); + } else if ("executedimmers".equals(cmd)) { + cmdExecuteDimmer(((QMessageMap) qMessageGson).getData()); + } else if ("executethermostat".equals(cmd)) { + cmdExecuteThermostat(((QMessageMap) qMessageGson).getData()); + } else if ("executescene".equals(cmd)) { + cmdExecuteScene(((QMessageMap) qMessageGson).getData()); + } else if ("executeslats".equals(cmd)) { + cmdExecuteSlats(((QMessageMap) qMessageGson).getData()); + } else if ("executerol".equals(cmd)) { + cmdExecuteRol(((QMessageMap) qMessageGson).getData()); + } else if ("executerol02pslats".equals(cmd)) { + cmdExecuteRolslats(((QMessageMap) qMessageGson).getData()); + } + // Incoming commands from Qbus Server to openHAB (event) + else if ("listbistabiel".equals(event)) { + eventListBistabiel(((QMessageListMap) qMessageGson).getData()); + } else if ("listdimmers".equals(event)) { + eventListDimmers(((QMessageListMap) qMessageGson).getData()); + } else if ("listthermostat".equals(event)) { + eventListThermostat(((QMessageListMap) qMessageGson).getData()); + } else if ("listscenes".equals(event)) { + eventListScenes(((QMessageListMap) qMessageGson).getData()); + } else if ("listco2".equals(event)) { + eventListCO2(((QMessageListMap) qMessageGson).getData()); + } else if ("listrol".equals(event)) { + eventListRol(((QMessageListMap) qMessageGson).getData()); + } else if ("listrol02pslats".equals(event)) { + eventListRolslats(((QMessageListMap) qMessageGson).getData()); + } + // + else if ("disconnect".equals(event)) { + eventFunction(((QMessageListMap) qMessageGson).getData()); + } + } + } catch (JsonParseException e) { + logger.debug("Qbus: not acted on unsupported json {}", qMessage); + } + } + + /** + * After setting up the communication with the Qbus Server, send all initialization messages. + *

+ * First send connect to connect with the Qbus Server application + * Get request for the Scenes + * Get request for Bistabiel/Timers/Intervals/Mono outputs + * Get request for Dimmers 1T and 2T + * Get request for Thermostats + * Get request for CO2 + * Get request for Shutters + * + * @throws IOException + * @throws InterruptedException + */ + private void initialize() throws IOException, InterruptedException { + Connect(); + sendAndReadMessage("listrol"); + sendAndReadMessage("listrol02pslats"); + sendAndReadMessage("listscenes"); + sendAndReadMessage("listbistabiel"); + sendAndReadMessage("listdimmers"); + sendAndReadMessage("listthermostats"); + sendAndReadMessage("listco2"); + } + + /** + * Initial connection to Qbus Server to open a communication channel + */ + private void Connect() { + @SuppressWarnings("null") + String confsn = this.bridgeCallBack.getSn(); + QMessageCmd qCmd = new QMessageCmd("openHAB").withSn(confsn); + sendMessage(qCmd); + } + + /** + * Send message to Qbus server and read response + */ + private void sendAndReadMessage(String command) throws IOException, InterruptedException { + @SuppressWarnings("null") + String confsn = this.bridgeCallBack.getSn(); + QMessageCmd qCmd = new QMessageCmd(command).withSn(confsn); + + sendMessage(qCmd); + + BufferedReader reader = this.qIn; + if (reader == null) { + throw new IOException("Cannot read from socket, reader not connected."); + } + readMessage(reader.readLine()); + } + + /** + * Get all the scenes from the Qbus server + * + * @param data + */ + private void cmdlistscenes(@Nullable List> data) { + logger.debug("Qbus: Scenes received from Qbus server"); + + if (data != null) { + for (Map scene : data) { + try { + int id = Integer.parseInt(scene.get("id")); + QbusScene Scene = new QbusScene(id); + Scene.setQComm(this); + this.scenes.put(id, Scene); + } catch (Exception e) { + logger.debug("Qbus: Error in json for Scenes"); + } + } + } + } + + /** + * Get all the CO2 outputs from the Qbus server + * + * @param data + */ + private void cmdlistco2(@Nullable List> data) { + logger.debug("Qbus: CO2 received from Qbus server"); + + if (data != null) { + for (Map co2 : data) { + + try { + int id = Integer.parseInt(co2.get("id")); + Integer co = 0; + co = Integer.parseInt(co2.get("value1")); + if (!this.co2.containsKey(id)) { + QbusCO2 CO2 = new QbusCO2(id); + this.co2.put(id, CO2); + this.co2.get(id).setState(co); + } else { + this.co2.get(id).setState(co); + } + } catch (Exception e) { + logger.debug("Qbus: Error in json for CO2"); + } + + } + } + } + + /** + * Get all the Positioning module outputs from the Qbus server + * + * @param data + */ + private void cmdlistrol(@Nullable List> data) { + logger.debug("Qbus: ROL02P received from Qbus server"); + + if (data != null) { + for (Map rol : data) { + int id = 0; + id = Integer.parseInt(rol.get("id")); + Integer rolpos = 0; + // Integer rolposslats = 0; + try { + rolpos = Integer.valueOf(rol.get("value1")); + // rolposslats = Integer.valueOf(rol.get("value2")); + } catch (Exception e) { + logger.debug("Qbus: Error in json for Rollershutter"); + } + + if (!this.Rol.containsKey(id)) { + QbusRol Rol = new QbusRol(id); + Rol.setQComm(this); + this.Rol.put(id, Rol); + this.Rol.get(id).setState(rolpos); + // this.Rol.get(id).setSlats(rolposslats); + } else { + this.Rol.get(id).setState(rolpos); + // this.Rol.get(id).setSlats(rolposslats); + } + } + } + } + + /** + * Get all the Positioning module outputs from the Qbus server + * + * @param data + */ + private void cmdlistrolslats(@Nullable List> data) { + logger.debug("Qbus: ROL02PSLATS received from Qbus server"); + + if (data != null) { + for (Map rol : data) { + int id = 0; + id = Integer.parseInt(rol.get("id")); + Integer rolpos = 0; + Integer rolposslats = 0; + try { + rolpos = Integer.valueOf(rol.get("value1")); + rolposslats = Integer.valueOf(rol.get("value2")); + } catch (Exception e) { + logger.debug("Qbus: Error in json for Rollershutter"); + } + + if (!this.Rol.containsKey(id)) { + QbusRol Rol = new QbusRol(id); + Rol.setQComm(this); + this.Rol.put(id, Rol); + this.Rol.get(id).setState(rolpos); + this.Rol.get(id).setSlats(rolposslats); + } else { + this.Rol.get(id).setState(rolpos); + this.Rol.get(id).setSlats(rolposslats); + } + } + } + } + + /** + * Get all the Bistabiel/Timers/Mono/Interval from the Qbus server + * + * @param data + */ + private void cmdListBistabiel(@Nullable List> data) { + logger.debug("Qbus: Bistabiel/Timers/Monos/Intervals received from Qbus server"); + + if (data != null) { + for (Map bistabiel : data) { + int id = 0; + int state = 0; + id = Integer.parseInt(bistabiel.get("id")); + try { + state = Integer.parseInt(bistabiel.get("value1")); + } catch (Exception e) { + logger.debug("Qbus: Error in json for Bistabiel"); + } + + if (!this.bistabiel.containsKey(id)) { + QbusBistabiel qBistabiel = new QbusBistabiel(id); + qBistabiel.setState(state); + qBistabiel.setQComm(this); + this.bistabiel.put(id, qBistabiel); + } else { + this.bistabiel.get(id).setState(state); + } + } + } + } + + /** + * Get all the Dimmer outputs from the Qbus server + * + * @param data + */ + private void cmdListDimmers(@Nullable List> data) { + logger.debug("Qbus: Dimmers received from the Qbus server"); + + if (data != null) { + for (Map dimmer : data) { + + int id = 0; + int state = 0; + id = Integer.parseInt(dimmer.get("id")); + try { + state = Integer.parseInt(dimmer.get("value1")); + } catch (Exception e) { + logger.debug("Qbus: Error in json for Dimmer"); + } + + if (!this.dimmer.containsKey(id)) { + QbusDimmer qDimmer = new QbusDimmer(id); + qDimmer.setState(state); + qDimmer.setQComm(this); + this.dimmer.put(id, qDimmer); + } else { + this.dimmer.get(id).setState(state); + } + } + } + } + + /** + * Get all the Thermostat outputs from the Qbus server + * + * @param data + */ + private void cmdListThermostat(@Nullable List> data) { + logger.debug("Qbus: thermostats received from the Qbus server"); + + if (data != null) { + for (Map thermostat : data) { + int id = Integer.parseInt(thermostat.get("id")); + Double measured = 0.0; + Double setpoint = 0.0; + int mode = 0; + try { + measured = Double.valueOf(thermostat.get("measured")); + setpoint = Double.valueOf(thermostat.get("setpoint")); + mode = Integer.valueOf(thermostat.get("mode")); + } catch (Exception e) { + logger.debug("Qbus: Error in json for Thermostat"); + } + + if (!this.thermostats.containsKey(id)) { + QThermostat qThermostat = new QThermostat(id); + qThermostat.updateState(measured, setpoint, mode); + qThermostat.setQComm(this); + this.thermostats.put(id, qThermostat); + } else { + this.thermostats.get(id).updateState(measured, setpoint, mode); + } + } + } + } + + /** + * Execute Bistabiel/Timers/Monos/Intervals + * + * @param data + */ + private void cmdExecuteBistabiel(Map data) { + Integer errorCode = Integer.valueOf(data.get("error")); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute bistabiel success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } + } + + /** + * Execute Scenes + * + * @param data + */ + private void cmdExecuteScene(Map data) { + Integer errorCode = Integer.valueOf(data.get("error")); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute scene success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } + } + + /** + * Execute Dimmers + * + * @param data + */ + private void cmdExecuteDimmer(Map data) { + Integer errorCode = Integer.valueOf(data.get("error")); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute dimmer success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } + } + + /** + * Execute Slats + * + * @param data + */ + private void cmdExecuteSlats(Map data) { + Integer errorCode = Integer.valueOf(data.get("error")); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute slats success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } + } + + /** + * Execute Shutter + * + * @param data + */ + private void cmdExecuteRol(Map data) { + Integer errorCode = Integer.valueOf(data.get("error")); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute shutter success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } + } + + /** + * Execute Shutter with slats + * + * @param data + */ + private void cmdExecuteRolslats(Map data) { + Integer errorCode = Integer.valueOf(data.get("error")); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute shutter with slats success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } + } + + /** + * Execute Thermostats + * + * @param data + */ + private void cmdExecuteThermostat(Map data) { + Integer errorCode = Integer.valueOf(data.get("error")); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute thermostat success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } + } + + /** + * Event on incomming Bistabiel/Timer/Mono/Interval updates + * + * @param data + */ + private void eventListBistabiel(List> data) { + for (Map bistabiel : data) { + int id = Integer.valueOf(bistabiel.get("id")); + if (!this.bistabiel.containsKey(id)) { + logger.warn("Qbus: bistabiel in controller not known {}", id); + return; + } + Integer state = Integer.valueOf(bistabiel.get("value1")); + logger.debug("Qbus: event execute bistabiel {} with state {}", id, state); + this.bistabiel.get(id).setState(state); + } + } + + /** + * Event on incomming CO2 updates + * + * @param data + */ + private void eventListCO2(List> data) { + for (Map co2 : data) { + int id = Integer.valueOf(co2.get("id")); + if (!this.co2.containsKey(id)) { + logger.warn("Qbus: co2 in controller not known {}", id); + return; + } + Integer state = Integer.valueOf(co2.get("value1")); + logger.debug("Qbus: event execute co2 {} with state {}", id, state); + this.co2.get(id).setState(state); + } + } + + /** + * Event on incomming ROL02P without updates + * + * @param data + */ + private void eventListRol(List> data) { + for (Map rol : data) { + int id = Integer.valueOf(rol.get("id")); + if (!this.Rol.containsKey(id)) { + logger.warn("Qbus: Rol02p in controller not known {}", id); + return; + } + Integer pos = Integer.valueOf(rol.get("pos")); + // Integer slat = Integer.valueOf(rol.get("slats")); + logger.debug("Qbus: event execute Rol02P {} with pos {}", id, pos); + this.Rol.get(id).setState(pos); + // this.Rol.get(id).setSlats(slat); + } + } + + /** + * Event on incomming ROL02P with slats updates + * + * @param data + */ + private void eventListRolslats(List> data) { + for (Map rol : data) { + int id = Integer.valueOf(rol.get("id")); + if (!this.Rol.containsKey(id)) { + logger.warn("Qbus: Rol02p in controller not known {}", id); + return; + } + Integer pos = Integer.valueOf(rol.get("pos")); + Integer slat = Integer.valueOf(rol.get("slats")); + logger.debug("Qbus: event execute ROL02P_Slats {} with pos {} and slats {}", id, pos, slat); + this.Rol.get(id).setState(pos); + this.Rol.get(id).setSlats(slat); + } + } + + /** + * Event on incomming Scene updates + * + * @param data + */ + private void eventListScenes(List> data) { + for (Map scene : data) { + int id = Integer.valueOf(scene.get("id")); + if (!this.scenes.containsKey(id)) { + logger.warn("Qbus: scene in controller not known {}", id); + return; + } + Integer state = Integer.valueOf(scene.get("value1")); + logger.debug("Qbus: event execute scene {} with state {}", id, state); + this.scenes.get(id).setState(state); + } + } + + /** + * Event on incomming Dimmer updates + * + * @param data + */ + private void eventListDimmers(List> data) { + for (Map dimmer : data) { + int id = Integer.valueOf(dimmer.get("id")); + if (!this.dimmer.containsKey(id)) { + logger.warn("Qbus: dimmer in controller not known {}", id); + return; + } + Integer state = Integer.valueOf(dimmer.get("value1")); + logger.debug("Qbus: event execute dimmer {} with state {}", id, state); + this.dimmer.get(id).setState(state); + } + } + + /** + * Event on incomming thermostat updates + * + * @param data + */ + private void eventListThermostat(List> data) { + for (Map thermostat : data) { + int id = Integer.parseInt(thermostat.get("id")); + if (!this.thermostats.containsKey(id)) { + logger.warn("Qbus: thermostat in controller not known {}", id); + return; + } + Double measured = Double.valueOf(thermostat.get("measured")); + Double setpoint = Double.valueOf(thermostat.get("setpoint")); + Integer mode = Integer.valueOf(thermostat.get("mode")); + logger.debug("Qbus: event execute thermostat {} with measured {}, setpoint {}, mode {}", id, measured, + setpoint, mode); + this.thermostats.get(id).updateState(measured, setpoint, mode); + } + } + + /* + * /** + * Event on Disconnect + * + * @param data + */ + private void eventFunction(List> data) { + // for (Map function : data) { + logger.debug("Disconnect"); + // String ctd = function.get("ctd"); + // String funcion = function.get("function"); + + QbusBridgeHandler handler = this.bridgeCallBack; + + if (handler != null) { + stopCommunication(); + handler.bridgeOffline(); + + } + } + + /** + * Called by other methods to send json cmd to Qbus. + * + * @param qMessage + */ + synchronized void sendMessage(Object qMessage) { + PrintWriter writer = this.qOut; + String json = gsonOut.toJson(qMessage); + logger.debug("Qbus: send json from thread {}", Thread.currentThread().getId()); + + if (writer != null) { + writer.println(json); + + try { + TimeUnit.MILLISECONDS.sleep(250); + } catch (InterruptedException e) { + // No reaction on error is required + } + + } + if ((writer == null) || (writer.checkError())) { + logger.warn("Qbus: error sending message, trying to restart communication"); + restartCommunication(); + // retry sending after restart + logger.debug("Qbus: resend json from thread {}", Thread.currentThread().getId()); + writer = this.qOut; + if (writer != null) { + writer.println(json); + } + if ((writer == null) || (writer.checkError())) { + logger.warn("Qbus: error resending message"); + + } + } + } + + /** + * Return all Bistabiel/Timers/Mono/Intervals in the Qbus Controller. + * + * @return + */ + public Map getBistabiel() { + return this.bistabiel; + } + + /** + * Return all Dimmers in the Qbus Controller. + * + * @return + */ + public Map getDimmer() { + return this.dimmer; + } + + /** + * Return all Scenes in the Qbus Controller + * + * @return + */ + public Map getScenes() { + return this.scenes; + } + + /** + * Return all Thermostats in the Qbus Controller. + * + * @return + */ + public Map getThermostats() { + return this.thermostats; + } + + /** + * Return all CO2 in the Qbus Controller. + * + * @return + */ + public Map getCo2() { + return this.co2; + } + + /** + * Return all ROL02P outûts in the Qbus Controller. + * + * @return + */ + public Map getRol() { + return this.Rol; + } + + /** + * @param bridgeCallBack the bridgeCallBack to set + */ + public void setBridgeCallBack(QbusBridgeHandler bridgeCallBack) { + this.bridgeCallBack = bridgeCallBack; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java new file mode 100644 index 0000000000000..b7fb67c1ef84f --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.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.qbus.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusDimmerHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusDimmer} class represents the action Qbus Dimmer output. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public final class QbusDimmer { + + private final Logger logger = LoggerFactory.getLogger(QbusDimmer.class); + + @Nullable + private QbusCommunication QComm; + + private int id; + private Integer state = 0; + + @Nullable + private QbusDimmerHandler thingHandler; + + QbusDimmer(int id) { + this.id = id; + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to this dimmer is initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the dimmer receives an update from the Qbus IP-interface. + * + * @param handler + */ + public void setThingHandler(QbusDimmerHandler handler) { + this.thingHandler = handler; + } + + /** + * This method sets a pointer to the QComm Dimmer of class {@link QbusCommuncation}. + * This is then used to be able to call back the sendCommand method in this class to send a command to the + * Qbus IP-interface when.. + * + * @param QComm + */ + public void setQComm(QbusCommunication QComm) { + this.QComm = QComm; + } + + /** + * Get state of dimmer. + * + * @return dimmer state + */ + public Integer getState() { + return this.state; + } + + /** + * Sets state of Dimmer. + * + * @param dimmer state + */ + void setState(int state) { + this.state = state; + QbusDimmerHandler handler = thingHandler; + if (handler != null) { + logger.debug("Qbus: update channel state for {} with {}", id, state); + handler.handleStateUpdate(this); + } + } + + /** + * Sends Dimmer state to Qbus. + */ + public void execute(int percent, String sn) { + logger.debug("Qbus: execute dimmer for {} on CTD {}", this.id, sn); + + QMessageCmd QCmd = new QMessageCmd("executedimmer", this.id, percent).withSn(sn); + + QbusCommunication comm = QComm; + if (comm != null) { + comm.sendMessage(QCmd); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java new file mode 100644 index 0000000000000..6b6b1bf71900a --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.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.qbus.internal.protocol; + +/** + * Class {@link QbusMessageBase} used as base class for output from gson for cmd or event feedback from the Qbus server. + * This class only contains the common base fields required for the deserializer + * {@link QbusMessageDeserializer} to select the specific formats implemented in {@link QbusMessageMap}, + * {@link QbusMessageListMap}, {@link QbusMessageCmd}. + *

+ * + * @author Koen Schockaert - Initial Contribution + */ + +abstract class QbusMessageBase { + + private String cmd = ""; + private String event1 = ""; + private String sn = ""; + + String getCmd() { + return this.cmd; + } + + void setCmd(String cmd) { + this.cmd = cmd; + } + + void setSn(String sn) { + this.sn = sn; + } + + String getEvent() { + return this.event1; + } + + void setEvent(String event) { + this.event1 = event; + } + + String getSn() { + return this.sn; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java new file mode 100644 index 0000000000000..eee52cd2fd3ca --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java @@ -0,0 +1,110 @@ +/** + * 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.qbus.internal.protocol; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; + +import com.google.gson.JsonArray; +import com.google.gson.JsonDeserializationContext; +import com.google.gson.JsonDeserializer; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.google.gson.JsonParseException; + +/** + * Class {@link QbusMessageDeserializer} deserializes all json messages from Qbus. Various json + * message formats are supported. The format is selected based on the content of the cmd and event json objects. + * + * @author Koen Schockaert - Initial Contribution + * + */ + +class QbusMessageDeserializer implements JsonDeserializer { + + @Override + public QbusMessageBase deserialize(final JsonElement json, final Type typeOfT, + final JsonDeserializationContext context) throws JsonParseException { + final JsonObject jsonObject = json.getAsJsonObject(); + + try { + + String cmd = null; + String event1 = null; + String sn = null; + + if (jsonObject.has("cmd")) { + cmd = jsonObject.get("cmd").getAsString(); + } + if (jsonObject.has("event1")) { + event1 = jsonObject.get("event1").getAsString(); + } + if (jsonObject.has("ctdsn")) { + sn = jsonObject.get("ctdsn").getAsString(); + } + + JsonElement jsonData = null; + if (jsonObject.has("data")) { + jsonData = jsonObject.get("data"); + } + + QbusMessageBase message = null; + + if (jsonData != null) { + if (jsonData.isJsonObject()) { + message = new QMessageMap(); + + Map data = new HashMap<>(); + for (Entry entry : jsonData.getAsJsonObject().entrySet()) { + data.put(entry.getKey(), entry.getValue().getAsString()); + } + ((QMessageMap) message).setData(data); + + } else if (jsonData.isJsonArray()) { + JsonArray jsonDataArray = jsonData.getAsJsonArray(); + + message = new QMessageListMap(); + + List> dataList = new ArrayList<>(); + for (int i = 0; i < jsonDataArray.size(); i++) { + JsonObject jsonDataObject = jsonDataArray.get(i).getAsJsonObject(); + + Map data = new HashMap<>(); + for (Entry entry : jsonDataObject.entrySet()) { + data.put(entry.getKey(), entry.getValue().getAsString()); + } + dataList.add(data); + } + ((QMessageListMap) message).setData(dataList); + } + } + + if (message != null) { + message.setCmd(cmd); + message.setEvent(event1); + message.setSn(sn); + } else { + throw new JsonParseException("Unexpected Json type"); + } + + return message; + + } catch (IllegalStateException | ClassCastException e) { + throw new JsonParseException("Unexpected Json type"); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java new file mode 100644 index 0000000000000..29d4cb6779c41 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java @@ -0,0 +1,141 @@ +/** + * 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.qbus.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusRolHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusRol} class represents the action Qbus Shutter/Slats output. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public final class QbusRol { + + private final Logger logger = LoggerFactory.getLogger(QbusRol.class); + + @Nullable + private QbusCommunication QComm; + + private int id; + private Integer state = 0; + private Integer slats = 0; + + @Nullable + private QbusRolHandler thingHandler; + + QbusRol(int id) { + this.id = id; + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to this Shutter/Slats is + * initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the shutter/slat receives an update from the Qbus IP-interface. + * + * @param qbusRolHandler + */ + public void setThingHandler(QbusRolHandler qbusRolHandler) { + this.thingHandler = qbusRolHandler; + } + + /** + * This method sets a pointer to the QComm Shutter/Slats of class {@link QbusCommuncation}. + * This is then used to be able to call back the sendCommand method in this class to send a command to the + * Qbus IP-interface when.. + * + * @param QComm + */ + public void setQComm(QbusCommunication QComm) { + this.QComm = QComm; + } + + /** + * Get state of shutter. + * + * @return shutter state + */ + public Integer getState() { + return this.state; + } + + /** + * Get state of slats. + * + * @return slats state + */ + public Integer getStateSlats() { + return this.slats; + } + + /** + * Sets state of Shutter. + * + * @param shutter state + */ + public void setState(Integer Slats) { + this.state = Slats; + QbusRolHandler handler = thingHandler; + if (handler != null) { + logger.debug("Qbus: update channel shutter for {} with {}", id, state); + handler.handleStateUpdate(this); + } + } + + /** + * Sets state of Slats. + * + * @param slats state + */ + public void setSlats(Integer Slats) { + this.slats = Slats; + QbusRolHandler handler = thingHandler; + if (handler != null) { + logger.debug("Qbus: update channel slats for {} with {}", id, slats); + handler.handleStateUpdate(this); + } + } + + /** + * Sends shutter to Qbus. + */ + public void execute(int percent, String sn) { + logger.debug("Qbus: execute position for {} {}", this.id, sn); + + QMessageCmd QCmd = new QMessageCmd("executestore", this.id, percent).withSn(sn); + + QbusCommunication comm = QComm; + if (comm != null) { + comm.sendMessage(QCmd); + } + } + + /** + * Sends slats to Qbus. + */ + public void executeSlats(int percent, String sn) { + logger.debug("Qbus: execute slats for {} {}", this.id, sn); + + QMessageCmd QCmd = new QMessageCmd("executeslats", this.id, percent).withSn(sn); + + QbusCommunication comm = QComm; + if (comm != null) { + comm.sendMessage(QCmd); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java new file mode 100644 index 0000000000000..6b58b37da3157 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java @@ -0,0 +1,103 @@ +/** + * 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.qbus.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.handler.QbusSceneHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link QbusScene} class represents the action Qbus Scene output. + * + * @author Koen Schockaert - Initial Contribution + */ +@NonNullByDefault +public final class QbusScene { + + private final Logger logger = LoggerFactory.getLogger(QbusScene.class); + + @Nullable + private QbusCommunication QComm; + + private int id; + private Integer state = 0; + + @Nullable + private QbusSceneHandler thingHandler; + + QbusScene(int id) { + this.id = id; + } + + /** + * This method should be called if the ThingHandler for the thing corresponding to this scene is initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the scene receives an update from the Qbus IP-interface. + * + * @param handler + */ + public void setThingHandler(QbusSceneHandler handler) { + this.thingHandler = handler; + } + + /** + * This method sets a pointer to the QComm Scene of class {@link QbusCommuncation}. + * This is then used to be able to call back the sendCommand method in this class to send a command to the + * Qbus IP-interface when.. + * + * @param QComm + */ + public void setQComm(QbusCommunication QComm) { + this.QComm = QComm; + } + + /** + * Get state of scene. + * + * @return scene state + */ + public Integer getState() { + return this.state; + } + + /** + * Sets state of Scene. + * + * @param scene state + */ + void setState(int state) { + this.state = state; + QbusSceneHandler handler = thingHandler; + if (handler != null) { + logger.debug("Qbus: update channel state for {} with {}", id, state); + handler.handleStateUpdate(this); + } + } + + /** + * Sends action to Qbus. + */ + public void execute(int val, String sn) { + logger.debug("Qbus: execute scene for {} on CTD {}", this.id, sn); + + QMessageCmd QCmd = new QMessageCmd("executescene", this.id, val).withSn(sn); + ; + + QbusCommunication comm = QComm; + if (comm != null) { + comm.sendMessage(QCmd); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 0000000000000..b18daa90afc8d --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,10 @@ + + + + Qbus Binding + This is the binding for Qbus. + Koen Schockaert + + diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties new file mode 100644 index 0000000000000..0c7193b3f16c7 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties @@ -0,0 +1,17 @@ +# FIXME: please substitute the xx_XX with a proper locale, ie. de_DE +# FIXME: please do not add the file to the repo if you add or change no content +# binding +binding.qbus.name = +binding.qbus.description = + +# thing types +thing-type.qbus.sample.label = +thing-type.qbus.sample.description = + +# thing type config description +thing-type.config.qbus.sample.config1.label = +thing-type.config.qbus.sample.config1.description = + +# channel types +channel-type.qbus.sample-channel.label = +channel-type.qbus.sample-channel.description = diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 0000000000000..a7aef153dc7e5 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,247 @@ + + + + + + This bridge represents a Qbus client + + + + IP Address of Qbus server, usually 'localhost' + localhost + false + network-address + + + + Serial nr of the CTD controller + + false + + + + Port to communicate with Qbus server, default 8447 + 8447 + true + + + + Refresh interval for connection with Qbus server (min), default 300. If set to 0 or left empty, no + refresh will be scheduled + 300 + true + + + + + + + + + + Bistabiel-Mono-Timer-Interval output + + + + + + + Qbus IP Interface Output Object ID + false + + + + + + + + + + Qbus scene + + + + + + + Qbus scene ID + false + + + + + + + + + + + Qbus CO2 + + + + + + + Qbus CO2 ID + false + + + + + + + + + + Qbus dimmer output + + + + + + + Qbus IP Interface Output ID + false + + + + Step value used for increase/decrease of dimmer brightness, default 10% + 10 + true + + + + + + + + + + + + Qbus Shutter control + + + + + + + + Qbus rol Id + false + + + + + + + + + + Qbus thermostat + + + + + + + + + Qbus IP Interface Thermostat ID + false + + + + + + Switch + + Scene control for Qbus + Scene + + + + Number + + CO2 value for Qbus + CO2 + + + + Switch + + Switch control for output in Qbus + Switch + + + + Dimmer + + Brightness control for dimmer function in Qbus + DimmableLight + + + + Color + + DMX control for dimmer function in Qbus + HSB + + + + Number:Temperature + + Temperature measured by thermostat + Temperature + + CurrentTemperature + + + + + + Number:Temperature + + Setpoint temperature of thermostat + Temperature + + TargetTemperature + + + + + + Number + + Thermostat mode + Number + + + + + + + + + + + + + Rollershutter + + Rollershutter control for Qbus + Blinds + + + + Dimmer + + Slatcontrol function in Qbus + Blinds + + + From 9507c35f42ecc0a182e320ca9f4d4130462315a0 Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Tue, 1 Dec 2020 11:28:21 +0100 Subject: [PATCH 04/17] Second commit Had some issues in first commit. Fixed the bugs. Signed-off-by: QbusKoen --- bundles/org.openhab.binding.qbus/README.md | 9 +- bundles/org.openhab.binding.qbus/pom.xml | 2 +- .../handler/QbusBistabielHandler.java | 71 ++- .../qbus/internal/handler/QbusCO2Handler.java | 70 ++- .../internal/handler/QbusDimmerHandler.java | 83 ++-- .../qbus/internal/handler/QbusRolHandler.java | 76 ++- .../internal/handler/QbusSceneHandler.java | 71 ++- .../handler/QbusThermostatHandler.java | 48 +- .../qbus/internal/protocol/QMessageCmd.java | 8 +- .../internal/protocol/QbusCommunication.java | 464 ++++++++++-------- .../qbus/internal/protocol/QbusDimmer.java | 15 + .../protocol/QbusMessageDeserializer.java | 4 +- .../qbus/internal/protocol/QbusScene.java | 2 +- bundles/pom.xml | 1 + 14 files changed, 499 insertions(+), 425 deletions(-) diff --git a/bundles/org.openhab.binding.qbus/README.md b/bundles/org.openhab.binding.qbus/README.md index 89b5efe07d84d..4f30c648a4f3e 100644 --- a/bundles/org.openhab.binding.qbus/README.md +++ b/bundles/org.openhab.binding.qbus/README.md @@ -1,4 +1,5 @@ # Qbus Binding + ![Qbus Logo](doc/Logo.JPG) This binding for Qbus communicates for all controllers of the Qbus home automation system. @@ -19,6 +20,7 @@ With this binding you can control and read almost every output from the Qbus sys ## Supported Things The following things are supported by the Qbus Binding: + - Dimmer 1 button, 2 button and clc as _dimmer_ - Bistabiel, Timer1-3, Interval as _onOff_ - Thermostats - normal and PID as _thermosats_ @@ -27,6 +29,7 @@ The following things are supported by the Qbus Binding: - Rollershutter and rollerhutter with slats as _rollershutter_ For now the folowing Qbus things are not yet supported but will come: + - DMX - Timer 4 & 5 - HVAC @@ -77,6 +80,7 @@ The Bridge connects to the QbusServer, so if the Client/Server application is in ## Full Example ### Things: + ``` Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, refresh=10 ] { dimmer 1 "ToonzaalLED" [ dimmerId=100 ] @@ -88,7 +92,9 @@ Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, refresh rollershutter_slats 121 "Roller2" [ rolId=264 ] } ``` + ### Items: + ``` Dimmer ToonzaalLED [ "Lighting" ] {channel="qbus:dimmer:CTD007841:1:brightness"} Switch Toonzaal230V {channel="qbus:onOff:CTD007841:30:switch"} @@ -100,4 +106,5 @@ Number ProductieCO2 {channel Rollershutter Roller1 {channel="qbus:rollershutter:CTD007841:120:rollershutter"} Rollershutter Roller2 {channel="qbus:rollershutter_slats:CTD007841:121:rollershutter"} Dimmer Roller2_slats {channel="qbus:rollershutter_slats:CTD007841:121:slats"} -``` \ No newline at end of file +``` + diff --git a/bundles/org.openhab.binding.qbus/pom.xml b/bundles/org.openhab.binding.qbus/pom.xml index 3cda2c4358f52..c611bfc48f111 100644 --- a/bundles/org.openhab.binding.qbus/pom.xml +++ b/bundles/org.openhab.binding.qbus/pom.xml @@ -7,7 +7,7 @@ org.openhab.addons.bundles org.openhab.addons.reactor.bundles - 3.0.0-SNAPSHOT + 3.1.0-SNAPSHOT org.openhab.binding.qbus diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java index fb45b86151077..fcdc07ed2c1ac 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java @@ -15,9 +15,6 @@ import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; import static org.openhab.core.types.RefreshType.REFRESH; -import java.util.HashMap; -import java.util.Map; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.qbus.internal.QbusBridgeHandler; import org.openhab.binding.qbus.internal.protocol.QbusBistabiel; @@ -40,13 +37,12 @@ * * @author Koen Schockaert - Initial Contribution */ + @NonNullByDefault public class QbusBistabielHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(QbusBistabielHandler.class); - // private volatile int prevBistabielState; - public QbusBistabielHandler(Thing thing) { super(thing); } @@ -77,26 +73,32 @@ public void handleCommand(ChannelUID channelUID, Command command) { QbusBistabiel QBistabiel = QComm.getBistabiel().get(BistabielId); - if (QComm.communicationActive()) { - handleCommandSelection(QBistabiel, channelUID, command); - } else { - // We lost connection but the connection object is there, so was correctly started. - // Try to restart communication. - // This can be expensive, therefore do it in a job. - scheduler.submit(() -> { - QComm.restartCommunication(); - // If still not active, take thing offline and return. - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - // Also put the bridge back online - QBridgeHandler.bridgeOnline(); - - // And finally handle the command + if (QBistabiel != null) { + if (QComm.communicationActive()) { handleCommandSelection(QBistabiel, channelUID, command); - }); + } else { + // We lost connection but the connection object is there, so was correctly started. + // Try to restart communication. + // This can be expensive, therefore do it in a job. + scheduler.submit(() -> { + QComm.restartCommunication(); + // If still not active, take thing offline and return. + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + // Also put the bridge back online + QBridgeHandler.bridgeOnline(); + + // And finally handle the command + handleCommandSelection(QBistabiel, channelUID, command); + }); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: BistabielId " + BistabielId + " does not match a BISTABIEL in the controller"); + return; } } @@ -154,18 +156,17 @@ public void initialize() { QbusBistabiel QBistabiel = QComm.getBistabiel().get(BistabielId); - // int bistabielState = QBistabiel.getState(); - - // this.prevBistabielState = bistabielState; - QBistabiel.setThingHandler(this); - - Map properties = new HashMap<>(); - - thing.setProperties(properties); + // Map properties = new HashMap<>(); - handleStateUpdate(QBistabiel); + // thing.setProperties(properties); - logger.debug("Qbus: bistabiel intialized {}", BistabielId); + if (QBistabiel != null) { + QBistabiel.setThingHandler(this); + handleStateUpdate(QBistabiel); + logger.info("Qbus: Bistabiel intialized {}", BistabielId); + } else { + logger.info("Qbus: Bistabiel not intialized {} - null", BistabielId); + } } /** @@ -177,7 +178,5 @@ public void handleStateUpdate(QbusBistabiel QBistabiel) { updateState(CHANNEL_SWITCH, (bistabielState == 0) ? OnOffType.OFF : OnOffType.ON); updateStatus(ThingStatus.ONLINE); - - // this.prevBistabielState = bistabielState; } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java index b500901e62073..26d19930585fb 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java @@ -15,9 +15,6 @@ import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; import static org.openhab.core.types.RefreshType.REFRESH; -import java.util.HashMap; -import java.util.Map; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.qbus.internal.QbusBridgeHandler; import org.openhab.binding.qbus.internal.protocol.QbusCO2; @@ -45,8 +42,6 @@ public class QbusCO2Handler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(QbusCO2Handler.class); - // private volatile int prevCO2State; - public QbusCO2Handler(Thing thing) { super(thing); } @@ -77,26 +72,32 @@ public void handleCommand(ChannelUID channelUID, Command command) { QbusCO2 QCO2 = QComm.getCo2().get(CO2Id); - if (QComm.communicationActive()) { - handleCommandSelection(QCO2, channelUID, command); - } else { - // We lost connection but the connection object is there, so was correctly started. - // Try to restart communication. - // This can be expensive, therefore do it in a job. - scheduler.submit(() -> { - QComm.restartCommunication(); - // If still not active, take thing offline and return. - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - // Also put the bridge back online - QBridgeHandler.bridgeOnline(); - - // And finally handle the command + if (QCO2 != null) { + if (QComm.communicationActive()) { handleCommandSelection(QCO2, channelUID, command); - }); + } else { + // We lost connection but the connection object is there, so was correctly started. + // Try to restart communication. + // This can be expensive, therefore do it in a job. + scheduler.submit(() -> { + QComm.restartCommunication(); + // If still not active, take thing offline and return. + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + // Also put the bridge back online + QBridgeHandler.bridgeOnline(); + + // And finally handle the command + handleCommandSelection(QCO2, channelUID, command); + }); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: CO2Id " + CO2Id + " does not match a CO2 in the controller"); + return; } } @@ -138,18 +139,17 @@ public void initialize() { QbusCO2 QCO2 = QComm.getCo2().get(CO2Id); - // int actionState = QCO2.getState(); - - // this.prevCO2State = actionState; - QCO2.setThingHandler(this); + // Map properties = new HashMap<>(); - Map properties = new HashMap<>(); + // thing.setProperties(properties); - thing.setProperties(properties); - - handleStateUpdate(QCO2); - - logger.debug("Qbus: CO2 intialized {}", CO2Id); + if (QCO2 != null) { + QCO2.setThingHandler(this); + handleStateUpdate(QCO2); + logger.info("Qbus: CO2 intialized {}", CO2Id); + } else { + logger.info("Qbus: CO2 not intialized {} - null", CO2Id); + } } /** @@ -161,7 +161,5 @@ public void handleStateUpdate(QbusCO2 QCO2) { updateState(CHANNEL_CO2, new DecimalType(CO2State)); updateStatus(ThingStatus.ONLINE); - - // this.prevCO2State = CO2State; } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java index 0ff3d2412fa00..584bf7a004e8a 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java @@ -15,9 +15,6 @@ import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; import static org.openhab.core.types.RefreshType.REFRESH; -import java.util.HashMap; -import java.util.Map; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.qbus.internal.QbusBridgeHandler; import org.openhab.binding.qbus.internal.protocol.QbusCommunication; @@ -47,8 +44,6 @@ public class QbusDimmerHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(QbusDimmerHandler.class); - // private volatile int prevDimmerState; - public QbusDimmerHandler(Thing thing) { super(thing); } @@ -79,33 +74,32 @@ public void handleCommand(ChannelUID channelUID, Command command) { QbusDimmer QDimmer = QComm.getDimmer().get(dimmerId); - /* - * if (QDimmer == null) { - * updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - * "Qbus: dimmerId " + dimmerId + " does not match a dimmer in the controller"); - * return; - * } - */ - if (QComm.communicationActive()) { - handleCommandSelection(QDimmer, channelUID, command); - } else { - // We lost connection but the connection object is there, so was correctly started. - // Try to restart communication. - // This can be expensive, therefore do it in a job. - scheduler.submit(() -> { - QComm.restartCommunication(); - // If still not active, take thing offline and return. - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - // Also put the bridge back online - QBridgeHandler.bridgeOnline(); - - // And finally handle the command + if (QDimmer != null) { + if (QComm.communicationActive()) { handleCommandSelection(QDimmer, channelUID, command); - }); + } else { + // We lost connection but the connection object is there, so was correctly started. + // Try to restart communication. + // This can be expensive, therefore do it in a job. + scheduler.submit(() -> { + QComm.restartCommunication(); + // If still not active, take thing offline and return. + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + // Also put the bridge back online + QBridgeHandler.bridgeOnline(); + + // And finally handle the command + handleCommandSelection(QDimmer, channelUID, command); + }); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: dimmerId " + dimmerId + " does not match a DIMMER in the controller"); + return; } } @@ -213,25 +207,18 @@ public void initialize() { } QbusDimmer QDimmer = QComm.getDimmer().get(dimmerId); - /* - * if (QDimmer == null) { - * updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - * "Qbus: dimmerId does not match an action in the server " + dimmerId); - * return; - * } - */ - // int actionState = QDimmer.getState(); - - // this.prevDimmerState = actionState; - QDimmer.setThingHandler(this); - Map properties = new HashMap<>(); + // Map properties = new HashMap<>(); - thing.setProperties(properties); + // thing.setProperties(properties); - handleStateUpdate(QDimmer); - - logger.debug("Qbus: dimmer intialized {}", dimmerId); + if (QDimmer != null) { + QDimmer.setThingHandler(this); + handleStateUpdate(QDimmer); + logger.info("Qbus: Dimmer intialized {}", dimmerId); + } else { + logger.info("Qbus: Dimmer not intialized {} - null", dimmerId); + } } /** @@ -244,7 +231,5 @@ public void handleStateUpdate(QbusDimmer QDimmer) { updateState(CHANNEL_BRIGHTNESS, new PercentType(dimmerState)); updateStatus(ThingStatus.ONLINE); - - // this.prevDimmerState = dimmerState; } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java index fa9bad35e3458..694909b8f6d9d 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java @@ -15,9 +15,6 @@ import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; import static org.openhab.core.types.RefreshType.REFRESH; -import java.util.HashMap; -import java.util.Map; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.qbus.internal.QbusBridgeHandler; import org.openhab.binding.qbus.internal.protocol.QbusCommunication; @@ -46,9 +43,6 @@ public class QbusRolHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(QbusRolHandler.class); - // private volatile int prevRolState; - // private volatile int prevSlatState; - public QbusRolHandler(Thing thing) { super(thing); } @@ -79,26 +73,32 @@ public void handleCommand(ChannelUID channelUID, Command command) { QbusRol QRol = QComm.getRol().get(RolId); - if (QComm.communicationActive()) { - handleCommandSelection(QRol, channelUID, command); - } else { - // We lost connection but the connection object is there, so was correctly started. - // Try to restart communication. - // This can be expensive, therefore do it in a job. - scheduler.submit(() -> { - QComm.restartCommunication(); - // If still not active, take thing offline and return. - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - // Also put the bridge back online - QBridgeHandler.bridgeOnline(); - - // And finally handle the command + if (QRol != null) { + if (QComm.communicationActive()) { handleCommandSelection(QRol, channelUID, command); - }); + } else { + // We lost connection but the connection object is there, so was correctly started. + // Try to restart communication. + // This can be expensive, therefore do it in a job. + scheduler.submit(() -> { + QComm.restartCommunication(); + // If still not active, take thing offline and return. + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + // Also put the bridge back online + QBridgeHandler.bridgeOnline(); + + // And finally handle the command + handleCommandSelection(QRol, channelUID, command); + }); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: RolId " + RolId + " does not match a ROL in the controller"); + return; } } @@ -227,23 +227,18 @@ public void initialize() { } QbusRol QRol = QComm.getRol().get(RolId); - /* - * int rolState = QRol.getState(); - * int slatState = QRol.getStateSlats(); - * - * this.prevRolState = rolState; - * this.prevSlatState = slatState; - * - */ - QRol.setThingHandler(this); - - Map properties = new HashMap<>(); - thing.setProperties(properties); + // Map properties = new HashMap<>(); - handleStateUpdate(QRol); + // thing.setProperties(properties); - logger.debug("Qbus: Slats intialized {}", RolId); + if (QRol != null) { + QRol.setThingHandler(this); + handleStateUpdate(QRol); + logger.info("Qbus: Dimmer intialized {}", RolId); + } else { + logger.info("Qbus: Dimmer not intialized {} - null", RolId); + } } /** @@ -257,8 +252,5 @@ public void handleStateUpdate(QbusRol qRol) { updateState(CHANNEL_ROLLERSHUTTER, new PercentType(rolState)); updateState(CHANNEL_SLATS, new PercentType(slatState)); updateStatus(ThingStatus.ONLINE); - - // this.prevRolState = rolState; - // this.prevSlatState = slatState; } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java index ca810f3bd4fe7..0a3bccd372920 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java @@ -15,9 +15,6 @@ import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; import static org.openhab.core.types.RefreshType.REFRESH; -import java.util.HashMap; -import java.util.Map; - import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.qbus.internal.QbusBridgeHandler; import org.openhab.binding.qbus.internal.protocol.QbusCommunication; @@ -45,8 +42,6 @@ public class QbusSceneHandler extends BaseThingHandler { private final Logger logger = LoggerFactory.getLogger(QbusSceneHandler.class); - // private volatile int prevSceneState; - public QbusSceneHandler(Thing thing) { super(thing); } @@ -77,26 +72,32 @@ public void handleCommand(ChannelUID channelUID, Command command) { QbusScene QScene = QComm.getScenes().get(SceneId); - if (QComm.communicationActive()) { - handleCommandSelection(QScene, channelUID, command); - } else { - // We lost connection but the connection object is there, so was correctly started. - // Try to restart communication. - // This can be expensive, therefore do it in a job. - scheduler.submit(() -> { - QComm.restartCommunication(); - // If still not active, take thing offline and return. - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - // Also put the bridge back online - QBridgeHandler.bridgeOnline(); - - // And finally handle the command + if (QScene != null) { + if (QComm.communicationActive()) { handleCommandSelection(QScene, channelUID, command); - }); + } else { + // We lost connection but the connection object is there, so was correctly started. + // Try to restart communication. + // This can be expensive, therefore do it in a job. + scheduler.submit(() -> { + QComm.restartCommunication(); + // If still not active, take thing offline and return. + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + // Also put the bridge back online + QBridgeHandler.bridgeOnline(); + + // And finally handle the command + handleCommandSelection(QScene, channelUID, command); + }); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: SceneId " + SceneId + " does not match a SCENE in the controller"); + return; } } @@ -152,30 +153,28 @@ public void initialize() { QbusScene QScene = QComm.getScenes().get(SceneId); - // int sceneState = QScene.getState(); - /* - * this.prevSceneState = sceneState; - * QScene.setThingHandler(this); - */ - Map properties = new HashMap<>(); - - thing.setProperties(properties); + // Map properties = new HashMap<>(); - handleStateUpdate(QScene); + // thing.setProperties(properties); - logger.debug("Qbus: scene intialized {}", SceneId); + if (QScene != null) { + QScene.setThingHandler(this); + handleStateUpdate(QScene); + logger.info("Qbus: Scene intialized {}", SceneId); + } else { + logger.info("Qbus: Scene not intialized {} - null", SceneId); + } } /** * Method to update state of channel, called from Qbus Scene. */ + public void handleStateUpdate(QbusScene QScene) { int sceneState = QScene.getState(); updateState(CHANNEL_SWITCH, (sceneState == 0) ? OnOffType.OFF : OnOffType.ON); updateStatus(ThingStatus.ONLINE); - - // this.prevSceneState = sceneState; } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java index 3120718e73e01..b3663df892800 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java @@ -77,19 +77,25 @@ public void handleCommand(ChannelUID channelUID, Command command) { QThermostat qThermostat = QComm.getThermostats().get(thermostatId); - if (QComm.communicationActive()) { - handleCommandSelection(qThermostat, channelUID, command); - } else { - scheduler.submit(() -> { - QComm.restartCommunication(); - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - QBridgeHandler.bridgeOnline(); + if (qThermostat != null) { + if (QComm.communicationActive()) { handleCommandSelection(qThermostat, channelUID, command); - }); + } else { + scheduler.submit(() -> { + QComm.restartCommunication(); + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Qbus: communication socket error"); + return; + } + QBridgeHandler.bridgeOnline(); + handleCommandSelection(qThermostat, channelUID, command); + }); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Qbus: thermostatId " + thermostatId + " does not match a THERMOSTAT in the controller"); + return; } } @@ -136,13 +142,13 @@ public void initialize() { Integer thermostatId = ((Number) config.get(CONFIG_THERMOSTAT_ID)).intValue(); - Bridge nhcBridge = getBridge(); - if (nhcBridge == null) { + Bridge QBrdige = getBridge(); + if (QBrdige == null) { updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, "Qbus: no bridge initialized for thermostat " + thermostatId); return; } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) nhcBridge.getHandler(); + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBrdige.getHandler(); if (QBridgeHandler == null) { updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, "Qbus: no bridge initialized for thermostat " + thermostatId); @@ -157,11 +163,17 @@ public void initialize() { QThermostat qThermostat = QComm.getThermostats().get(thermostatId); - qThermostat.setThingHandler(this); + // Map properties = new HashMap<>(); - handleStateUpdate(qThermostat); + // thing.setProperties(properties); - logger.debug("Qbus: thermostat intialized {}", thermostatId); + if (qThermostat != null) { + qThermostat.setThingHandler(this); + handleStateUpdate(qThermostat); + logger.info("Qbus: Thermostat intialized {}", thermostatId); + } else { + logger.info("Qbus: Thermostat not intialized {} - null", thermostatId); + } } /** diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java index 78b89d7543ef5..f8d3b27b0e4ab 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java @@ -28,7 +28,6 @@ class QMessageCmd extends QbusMessageBase { private Integer pos; private Integer value1; private Integer value2; - private Integer value3; private Integer mode; private Double setpoint; private String ctdsn; @@ -52,12 +51,6 @@ class QMessageCmd extends QbusMessageBase { this.value2 = value2; } - QMessageCmd(String cmd, int id, Integer value1, Integer value2, Integer value3) { - this(cmd, id, value1); - this.value2 = value2; - this.value3 = value3; - } - QMessageCmd withMode(Integer mode) { this.mode = mode; return this; @@ -69,6 +62,7 @@ QMessageCmd withSetpoint(Double d) { } QMessageCmd withSn(String sn) { + this.ctdsn = sn; return this; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java index 47c2a115ffedc..dda251ad82894 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java @@ -248,13 +248,11 @@ private void readMessage(String qMessage) { @SuppressWarnings("null") String confsn = this.bridgeCallBack.getSn(); + @SuppressWarnings("null") String sn = qMessageGson.getSn(); cmd = qMessageGson.getCmd(); event = qMessageGson.getEvent(); - // logger.debug(cmd); - // logger.debug(event); - if (Integer.parseInt(confsn) == Integer.parseInt(sn)) { // Get the compatible outputs from the Qbus server if ("listbistabiel".equals(cmd)) { @@ -341,12 +339,15 @@ private void initialize() throws IOException, InterruptedException { /** * Initial connection to Qbus Server to open a communication channel + * + * @throws InterruptedException */ - private void Connect() { + private void Connect() throws InterruptedException { @SuppressWarnings("null") String confsn = this.bridgeCallBack.getSn(); QMessageCmd qCmd = new QMessageCmd("openHAB").withSn(confsn); sendMessage(qCmd); + TimeUnit.MILLISECONDS.sleep(250); } /** @@ -376,13 +377,18 @@ private void cmdlistscenes(@Nullable List> data) { if (data != null) { for (Map scene : data) { - try { - int id = Integer.parseInt(scene.get("id")); - QbusScene Scene = new QbusScene(id); - Scene.setQComm(this); - this.scenes.put(id, Scene); - } catch (Exception e) { - logger.debug("Qbus: Error in json for Scenes"); + String idStr = scene.get("id"); + if (idStr != null) { + try { + int id = Integer.parseInt(idStr); + QbusScene Scene = new QbusScene(id); + Scene.setQComm(this); + this.scenes.put(id, Scene); + } catch (Exception e) { + logger.debug("Qbus: Error in json for Scenes"); + } + } else { + logger.debug("Qbus: Error in json for Scenes Nullvalue"); } } } @@ -393,16 +399,19 @@ private void cmdlistscenes(@Nullable List> data) { * * @param data */ + @SuppressWarnings("null") private void cmdlistco2(@Nullable List> data) { logger.debug("Qbus: CO2 received from Qbus server"); if (data != null) { for (Map co2 : data) { + String idStr = co2.get("id"); + String value1Str = co2.get("value1"); + if (idStr != null && value1Str != null) { + + int id = Integer.parseInt(idStr); + Integer co = Integer.parseInt(value1Str); - try { - int id = Integer.parseInt(co2.get("id")); - Integer co = 0; - co = Integer.parseInt(co2.get("value1")); if (!this.co2.containsKey(id)) { QbusCO2 CO2 = new QbusCO2(id); this.co2.put(id, CO2); @@ -410,8 +419,9 @@ private void cmdlistco2(@Nullable List> data) { } else { this.co2.get(id).setState(co); } - } catch (Exception e) { - logger.debug("Qbus: Error in json for CO2"); + + } else { + logger.debug("Qbus: Error in json for CO2 Nullvalue"); } } @@ -423,31 +433,27 @@ private void cmdlistco2(@Nullable List> data) { * * @param data */ + @SuppressWarnings("null") private void cmdlistrol(@Nullable List> data) { logger.debug("Qbus: ROL02P received from Qbus server"); if (data != null) { for (Map rol : data) { - int id = 0; - id = Integer.parseInt(rol.get("id")); - Integer rolpos = 0; - // Integer rolposslats = 0; - try { - rolpos = Integer.valueOf(rol.get("value1")); - // rolposslats = Integer.valueOf(rol.get("value2")); - } catch (Exception e) { - logger.debug("Qbus: Error in json for Rollershutter"); - } - - if (!this.Rol.containsKey(id)) { - QbusRol Rol = new QbusRol(id); - Rol.setQComm(this); - this.Rol.put(id, Rol); - this.Rol.get(id).setState(rolpos); - // this.Rol.get(id).setSlats(rolposslats); + String idStr = rol.get("id"); + String value1Str = rol.get("value1"); + if (idStr != null && value1Str != null) { + int id = Integer.parseInt(idStr); + Integer rolpos = Integer.valueOf(value1Str); + if (!this.Rol.containsKey(id)) { + QbusRol Rol = new QbusRol(id); + Rol.setQComm(this); + this.Rol.put(id, Rol); + this.Rol.get(id).setState(rolpos); + } else { + this.Rol.get(id).setState(rolpos); + } } else { - this.Rol.get(id).setState(rolpos); - // this.Rol.get(id).setSlats(rolposslats); + logger.debug("Qbus: Error in json for ROL02P Nullvalue"); } } } @@ -458,31 +464,34 @@ private void cmdlistrol(@Nullable List> data) { * * @param data */ + @SuppressWarnings("null") private void cmdlistrolslats(@Nullable List> data) { logger.debug("Qbus: ROL02PSLATS received from Qbus server"); if (data != null) { for (Map rol : data) { - int id = 0; - id = Integer.parseInt(rol.get("id")); - Integer rolpos = 0; - Integer rolposslats = 0; - try { - rolpos = Integer.valueOf(rol.get("value1")); - rolposslats = Integer.valueOf(rol.get("value2")); - } catch (Exception e) { - logger.debug("Qbus: Error in json for Rollershutter"); - } - if (!this.Rol.containsKey(id)) { - QbusRol Rol = new QbusRol(id); - Rol.setQComm(this); - this.Rol.put(id, Rol); - this.Rol.get(id).setState(rolpos); - this.Rol.get(id).setSlats(rolposslats); + String idStr = rol.get("id"); + String value1Str = rol.get("value1"); + String value2Str = rol.get("value2"); + if (idStr != null && value1Str != null && value2Str != null) { + int id = Integer.parseInt(idStr); + Integer rolpos = Integer.valueOf(value1Str); + Integer rolposslats = Integer.valueOf(value2Str); + rolpos = Integer.valueOf(rolpos); + rolposslats = Integer.valueOf(rolposslats); + if (!this.Rol.containsKey(id)) { + QbusRol Rol = new QbusRol(id); + Rol.setQComm(this); + this.Rol.put(id, Rol); + this.Rol.get(id).setState(rolpos); + this.Rol.get(id).setSlats(rolposslats); + } else { + this.Rol.get(id).setState(rolpos); + this.Rol.get(id).setSlats(rolposslats); + } } else { - this.Rol.get(id).setState(rolpos); - this.Rol.get(id).setSlats(rolposslats); + logger.debug("Qbus: Error in json for ROL02P_Slats Nullvalue"); } } } @@ -493,27 +502,25 @@ private void cmdlistrolslats(@Nullable List> data) { * * @param data */ + @SuppressWarnings("null") private void cmdListBistabiel(@Nullable List> data) { logger.debug("Qbus: Bistabiel/Timers/Monos/Intervals received from Qbus server"); if (data != null) { for (Map bistabiel : data) { - int id = 0; - int state = 0; - id = Integer.parseInt(bistabiel.get("id")); - try { - state = Integer.parseInt(bistabiel.get("value1")); - } catch (Exception e) { - logger.debug("Qbus: Error in json for Bistabiel"); - } - - if (!this.bistabiel.containsKey(id)) { - QbusBistabiel qBistabiel = new QbusBistabiel(id); - qBistabiel.setState(state); - qBistabiel.setQComm(this); - this.bistabiel.put(id, qBistabiel); - } else { - this.bistabiel.get(id).setState(state); + String idStr = bistabiel.get("id"); + String value1Str = bistabiel.get("value1"); + if (idStr != null && value1Str != null) { + int id = Integer.parseInt(idStr); + Integer state = Integer.parseInt(value1Str); + if (!this.bistabiel.containsKey(id)) { + QbusBistabiel qBistabiel = new QbusBistabiel(id); + qBistabiel.setState(state); + qBistabiel.setQComm(this); + this.bistabiel.put(id, qBistabiel); + } else { + this.bistabiel.get(id).setState(state); + } } } } @@ -524,28 +531,25 @@ private void cmdListBistabiel(@Nullable List> data) { * * @param data */ + @SuppressWarnings("null") private void cmdListDimmers(@Nullable List> data) { logger.debug("Qbus: Dimmers received from the Qbus server"); if (data != null) { for (Map dimmer : data) { - - int id = 0; - int state = 0; - id = Integer.parseInt(dimmer.get("id")); - try { - state = Integer.parseInt(dimmer.get("value1")); - } catch (Exception e) { - logger.debug("Qbus: Error in json for Dimmer"); - } - - if (!this.dimmer.containsKey(id)) { - QbusDimmer qDimmer = new QbusDimmer(id); - qDimmer.setState(state); - qDimmer.setQComm(this); - this.dimmer.put(id, qDimmer); - } else { - this.dimmer.get(id).setState(state); + String idStr = dimmer.get("id"); + String value1Str = dimmer.get("value1"); + if (idStr != null && value1Str != null) { + int id = Integer.parseInt(idStr); + Integer state = Integer.parseInt(value1Str); + if (!this.dimmer.containsKey(id)) { + QbusDimmer qDimmer = new QbusDimmer(id); + qDimmer.updateState(state); + qDimmer.setQComm(this); + this.dimmer.put(id, qDimmer); + } else { + this.dimmer.get(id).updateState(state); + } } } } @@ -556,30 +560,30 @@ private void cmdListDimmers(@Nullable List> data) { * * @param data */ + @SuppressWarnings("null") private void cmdListThermostat(@Nullable List> data) { logger.debug("Qbus: thermostats received from the Qbus server"); if (data != null) { for (Map thermostat : data) { - int id = Integer.parseInt(thermostat.get("id")); - Double measured = 0.0; - Double setpoint = 0.0; - int mode = 0; - try { - measured = Double.valueOf(thermostat.get("measured")); - setpoint = Double.valueOf(thermostat.get("setpoint")); - mode = Integer.valueOf(thermostat.get("mode")); - } catch (Exception e) { - logger.debug("Qbus: Error in json for Thermostat"); - } - - if (!this.thermostats.containsKey(id)) { - QThermostat qThermostat = new QThermostat(id); - qThermostat.updateState(measured, setpoint, mode); - qThermostat.setQComm(this); - this.thermostats.put(id, qThermostat); - } else { - this.thermostats.get(id).updateState(measured, setpoint, mode); + String idStr = thermostat.get("id"); + String measuredStr = thermostat.get("measured"); + String setpointStr = thermostat.get("setpoint"); + String modeStr = thermostat.get("mode"); + if (idStr != null && measuredStr != null && setpointStr != null && modeStr != null) { + int id = Integer.parseInt(idStr); + Double measured = Double.valueOf(measuredStr); + Double setpoint = Double.valueOf(setpointStr); + Integer mode = Integer.valueOf(modeStr); + + if (!this.thermostats.containsKey(id)) { + QThermostat qThermostat = new QThermostat(id); + qThermostat.updateState(measured, setpoint, mode); + qThermostat.setQComm(this); + this.thermostats.put(id, qThermostat); + } else { + this.thermostats.get(id).updateState(measured, setpoint, mode); + } } } } @@ -591,11 +595,14 @@ private void cmdListThermostat(@Nullable List> data) { * @param data */ private void cmdExecuteBistabiel(Map data) { - Integer errorCode = Integer.valueOf(data.get("error")); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute bistabiel success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); + String errorCodeStr = data.get("error"); + if (errorCodeStr != null) { + Integer errorCode = Integer.valueOf(errorCodeStr); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute bistabiel success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } } } @@ -605,11 +612,14 @@ private void cmdExecuteBistabiel(Map data) { * @param data */ private void cmdExecuteScene(Map data) { - Integer errorCode = Integer.valueOf(data.get("error")); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute scene success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); + String errorCodeStr = data.get("error"); + if (errorCodeStr != null) { + Integer errorCode = Integer.valueOf(errorCodeStr); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute scene success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } } } @@ -619,11 +629,14 @@ private void cmdExecuteScene(Map data) { * @param data */ private void cmdExecuteDimmer(Map data) { - Integer errorCode = Integer.valueOf(data.get("error")); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute dimmer success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); + String errorCodeStr = data.get("error"); + if (errorCodeStr != null) { + Integer errorCode = Integer.valueOf(errorCodeStr); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute dimmer success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } } } @@ -633,11 +646,14 @@ private void cmdExecuteDimmer(Map data) { * @param data */ private void cmdExecuteSlats(Map data) { - Integer errorCode = Integer.valueOf(data.get("error")); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute slats success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); + String errorCodeStr = data.get("error"); + if (errorCodeStr != null) { + Integer errorCode = Integer.valueOf(errorCodeStr); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute slats success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } } } @@ -647,11 +663,14 @@ private void cmdExecuteSlats(Map data) { * @param data */ private void cmdExecuteRol(Map data) { - Integer errorCode = Integer.valueOf(data.get("error")); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute shutter success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); + String errorCodeStr = data.get("error"); + if (errorCodeStr != null) { + Integer errorCode = Integer.valueOf(errorCodeStr); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute shutter success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } } } @@ -661,11 +680,14 @@ private void cmdExecuteRol(Map data) { * @param data */ private void cmdExecuteRolslats(Map data) { - Integer errorCode = Integer.valueOf(data.get("error")); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute shutter with slats success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); + String errorCodeStr = data.get("error"); + if (errorCodeStr != null) { + Integer errorCode = Integer.valueOf(errorCodeStr); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute shutter with slats success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } } } @@ -675,11 +697,14 @@ private void cmdExecuteRolslats(Map data) { * @param data */ private void cmdExecuteThermostat(Map data) { - Integer errorCode = Integer.valueOf(data.get("error")); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute thermostat success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); + String errorCodeStr = data.get("error"); + if (errorCodeStr != null) { + Integer errorCode = Integer.valueOf(errorCodeStr); + if (errorCode.equals(0)) { + logger.debug("Qbus: execute thermostat success"); + } else { + logger.warn("Qbus: error code {} returned on command execution", errorCode); + } } } @@ -688,16 +713,21 @@ private void cmdExecuteThermostat(Map data) { * * @param data */ + @SuppressWarnings("null") private void eventListBistabiel(List> data) { for (Map bistabiel : data) { - int id = Integer.valueOf(bistabiel.get("id")); - if (!this.bistabiel.containsKey(id)) { - logger.warn("Qbus: bistabiel in controller not known {}", id); - return; + String idStr = bistabiel.get("id"); + String value1Str = bistabiel.get("value1Str"); + if (idStr != null && value1Str != null) { + int id = Integer.valueOf(idStr); + int value1 = Integer.valueOf(value1Str); + if (!this.bistabiel.containsKey(id)) { + logger.warn("Qbus: bistabiel in controller not known {}", id); + return; + } + logger.debug("Qbus: event execute bistabiel {} with state {}", id, value1); + this.bistabiel.get(id).setState(value1); } - Integer state = Integer.valueOf(bistabiel.get("value1")); - logger.debug("Qbus: event execute bistabiel {} with state {}", id, state); - this.bistabiel.get(id).setState(state); } } @@ -706,16 +736,21 @@ private void eventListBistabiel(List> data) { * * @param data */ + @SuppressWarnings("null") private void eventListCO2(List> data) { for (Map co2 : data) { - int id = Integer.valueOf(co2.get("id")); - if (!this.co2.containsKey(id)) { - logger.warn("Qbus: co2 in controller not known {}", id); - return; + String idStr = co2.get("id"); + String value1Str = co2.get("value1Str"); + if (idStr != null && value1Str != null) { + int id = Integer.valueOf(idStr); + int value1 = Integer.valueOf(value1Str); + if (!this.co2.containsKey(id)) { + logger.warn("Qbus: co2 in controller not known {}", id); + return; + } + logger.debug("Qbus: event execute co2 {} with state {}", id, value1); + this.co2.get(id).setState(value1); } - Integer state = Integer.valueOf(co2.get("value1")); - logger.debug("Qbus: event execute co2 {} with state {}", id, state); - this.co2.get(id).setState(state); } } @@ -724,18 +759,24 @@ private void eventListCO2(List> data) { * * @param data */ + @SuppressWarnings("null") private void eventListRol(List> data) { for (Map rol : data) { - int id = Integer.valueOf(rol.get("id")); - if (!this.Rol.containsKey(id)) { - logger.warn("Qbus: Rol02p in controller not known {}", id); - return; + + String idStr = rol.get("id"); + String value1Str = rol.get("value1Str"); + if (idStr != null && value1Str != null) { + int id = Integer.valueOf(idStr); + int value1 = Integer.valueOf(value1Str); + + if (!this.Rol.containsKey(id)) { + logger.warn("Qbus: Rol02p in controller not known {}", id); + return; + } + logger.debug("Qbus: event execute Rol02P {} with pos {}", id, value1); + this.Rol.get(id).setState(value1); + } - Integer pos = Integer.valueOf(rol.get("pos")); - // Integer slat = Integer.valueOf(rol.get("slats")); - logger.debug("Qbus: event execute Rol02P {} with pos {}", id, pos); - this.Rol.get(id).setState(pos); - // this.Rol.get(id).setSlats(slat); } } @@ -744,18 +785,25 @@ private void eventListRol(List> data) { * * @param data */ + @SuppressWarnings("null") private void eventListRolslats(List> data) { for (Map rol : data) { - int id = Integer.valueOf(rol.get("id")); - if (!this.Rol.containsKey(id)) { - logger.warn("Qbus: Rol02p in controller not known {}", id); - return; + String idStr = rol.get("id"); + String value1Str = rol.get("pos"); + String value2Str = rol.get("slats"); + if (idStr != null && value1Str != null && value2Str != null) { + int id = Integer.valueOf(idStr); + int value1 = Integer.valueOf(value1Str); + int value2 = Integer.valueOf(value2Str); + if (!this.Rol.containsKey(id)) { + logger.warn("Qbus: Rol02p in controller not known {}", id); + return; + } + + logger.debug("Qbus: event execute ROL02P_Slats {} with pos {} and slats {}", id, value1, value2); + this.Rol.get(id).setState(value1); + this.Rol.get(id).setSlats(value2); } - Integer pos = Integer.valueOf(rol.get("pos")); - Integer slat = Integer.valueOf(rol.get("slats")); - logger.debug("Qbus: event execute ROL02P_Slats {} with pos {} and slats {}", id, pos, slat); - this.Rol.get(id).setState(pos); - this.Rol.get(id).setSlats(slat); } } @@ -764,16 +812,22 @@ private void eventListRolslats(List> data) { * * @param data */ + @SuppressWarnings("null") private void eventListScenes(List> data) { for (Map scene : data) { - int id = Integer.valueOf(scene.get("id")); - if (!this.scenes.containsKey(id)) { - logger.warn("Qbus: scene in controller not known {}", id); - return; + String idStr = scene.get("id"); + String value1Str = scene.get("value1"); + if (idStr != null && value1Str != null) { + int id = Integer.valueOf(idStr); + int value1 = Integer.valueOf(value1Str); + if (!this.scenes.containsKey(id)) { + logger.warn("Qbus: scene in controller not known {}", id); + return; + } + + logger.debug("Qbus: event execute scene {} with state {}", id, value1); + this.scenes.get(id).setState(value1); } - Integer state = Integer.valueOf(scene.get("value1")); - logger.debug("Qbus: event execute scene {} with state {}", id, state); - this.scenes.get(id).setState(state); } } @@ -782,16 +836,23 @@ private void eventListScenes(List> data) { * * @param data */ + @SuppressWarnings("null") private void eventListDimmers(List> data) { for (Map dimmer : data) { - int id = Integer.valueOf(dimmer.get("id")); - if (!this.dimmer.containsKey(id)) { - logger.warn("Qbus: dimmer in controller not known {}", id); - return; + String idStr = dimmer.get("id"); + String value1Str = dimmer.get("value1"); + if (idStr != null && value1Str != null) { + int id = Integer.valueOf(idStr); + int value1 = Integer.valueOf(value1Str); + + if (!this.dimmer.containsKey(id)) { + logger.warn("Qbus: dimmer in controller not known {}", id); + return; + } + + logger.debug("Qbus: event execute dimmer {} with state {}", id, value1); + this.dimmer.get(id).setState(value1); } - Integer state = Integer.valueOf(dimmer.get("value1")); - logger.debug("Qbus: event execute dimmer {} with state {}", id, state); - this.dimmer.get(id).setState(state); } } @@ -800,19 +861,28 @@ private void eventListDimmers(List> data) { * * @param data */ + @SuppressWarnings("null") private void eventListThermostat(List> data) { for (Map thermostat : data) { - int id = Integer.parseInt(thermostat.get("id")); - if (!this.thermostats.containsKey(id)) { - logger.warn("Qbus: thermostat in controller not known {}", id); - return; + String idStr = thermostat.get("id"); + String measuredStr = thermostat.get("measured"); + String setpointdStr = thermostat.get("setpoint"); + String modedStr = thermostat.get("mode"); + if (idStr != null && measuredStr != null && setpointdStr != null && modedStr != null) { + int id = Integer.valueOf(idStr); + Double measured = Double.valueOf(measuredStr); + Double setpoint = Double.valueOf(setpointdStr); + Integer mode = Integer.valueOf(modedStr); + + if (!this.thermostats.containsKey(id)) { + logger.warn("Qbus: thermostat in controller not known {}", id); + return; + } + + logger.debug("Qbus: event execute thermostat {} with measured {}, setpoint {}, mode {}", id, measured, + setpoint, mode); + this.thermostats.get(id).updateState(measured, setpoint, mode); } - Double measured = Double.valueOf(thermostat.get("measured")); - Double setpoint = Double.valueOf(thermostat.get("setpoint")); - Integer mode = Integer.valueOf(thermostat.get("mode")); - logger.debug("Qbus: event execute thermostat {} with measured {}, setpoint {}, mode {}", id, measured, - setpoint, mode); - this.thermostats.get(id).updateState(measured, setpoint, mode); } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java index b7fb67c1ef84f..d852491dc7704 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java @@ -41,6 +41,21 @@ public final class QbusDimmer { this.id = id; } + /** + * Update all values of the dimmer + * + * @param state + */ + public void updateState(Integer state) { + setState(state); + + QbusDimmerHandler handler = thingHandler; + if (handler != null) { + logger.debug("Qbus: update channels for {}", id); + handler.handleStateUpdate(this); + } + } + /** * This method should be called if the ThingHandler for the thing corresponding to this dimmer is initialized. * It keeps a record of the thing handler in this object so the thing can be updated when diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java index eee52cd2fd3ca..73f76050aa548 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java @@ -19,6 +19,8 @@ import java.util.Map; import java.util.Map.Entry; +import org.eclipse.jdt.annotation.Nullable; + import com.google.gson.JsonArray; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; @@ -37,7 +39,7 @@ class QbusMessageDeserializer implements JsonDeserializer { @Override - public QbusMessageBase deserialize(final JsonElement json, final Type typeOfT, + public @Nullable QbusMessageBase deserialize(final JsonElement json, final Type typeOfT, final JsonDeserializationContext context) throws JsonParseException { final JsonObject jsonObject = json.getAsJsonObject(); diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java index 6b58b37da3157..f97c7eb82fa87 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java @@ -31,7 +31,7 @@ public final class QbusScene { @Nullable private QbusCommunication QComm; - private int id; + private int id = 0; private Integer state = 0; @Nullable diff --git a/bundles/pom.xml b/bundles/pom.xml index 394835617a4c7..f2aea0b7d17cd 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -240,6 +240,7 @@ org.openhab.binding.pulseaudio org.openhab.binding.pushbullet org.openhab.binding.pushover + org.openhab.binding.qbus org.openhab.binding.radiothermostat org.openhab.binding.regoheatpump org.openhab.binding.revogi From 9de4acb05e3abed6d9cfe71d11378b535bebad3a Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Sun, 3 Jan 2021 09:10:40 +0100 Subject: [PATCH 05/17] Solved issues Solved issues reported by fwolter and changed a lot of code, based on the comments. Signed-off-by: QbusKoen modified: CODEOWNERS Signed-off-by: QbusKoen --- CODEOWNERS | 1 + bundles/org.openhab.binding.qbus/README.md | 68 +- .../qbus/internal/QbusBindingConstants.java | 40 +- .../qbus/internal/QbusBridgeHandler.java | 238 +++-- .../qbus/internal/QbusConfiguration.java | 30 + .../qbus/internal/QbusHandlerFactory.java | 6 +- .../handler/QbusBistabielHandler.java | 179 ++-- .../qbus/internal/handler/QbusCO2Handler.java | 160 ++- .../internal/handler/QbusDimmerHandler.java | 206 ++-- .../internal/handler/QbusGlobalHandler.java | 93 ++ .../qbus/internal/handler/QbusRolHandler.java | 207 ++-- .../internal/handler/QbusSceneHandler.java | 182 ++-- .../handler/QbusThermostatHandler.java | 215 ++-- .../internal/handler/QbusThingsConfig.java | 31 + .../qbus/internal/protocol/QMessageCmd.java | 69 -- .../qbus/internal/protocol/QbusBistabiel.java | 13 +- .../qbus/internal/protocol/QbusCO2.java | 15 +- .../internal/protocol/QbusCommunication.java | 939 ++++++++---------- .../qbus/internal/protocol/QbusDimmer.java | 13 +- .../internal/protocol/QbusMessageBase.java | 58 +- .../internal/protocol/QbusMessageCmd.java | 62 ++ .../protocol/QbusMessageDeserializer.java | 61 +- ...geListMap.java => QbusMessageListMap.java} | 15 +- .../{QMessageMap.java => QbusMessageMap.java} | 14 +- .../qbus/internal/protocol/QbusRol.java | 21 +- .../qbus/internal/protocol/QbusScene.java | 14 +- .../{QThermostat.java => QbusThermostat.java} | 23 +- 27 files changed, 1545 insertions(+), 1428 deletions(-) create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java delete mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java create mode 100644 bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java rename bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/{QMessageListMap.java => QbusMessageListMap.java} (69%) rename bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/{QMessageMap.java => QbusMessageMap.java} (72%) rename bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/{QThermostat.java => QbusThermostat.java} (84%) diff --git a/CODEOWNERS b/CODEOWNERS index 363278450ac17..572a9138b11c6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -209,6 +209,7 @@ /bundles/org.openhab.binding.pulseaudio/ @peuter /bundles/org.openhab.binding.pushbullet/ @hakan42 /bundles/org.openhab.binding.pushover/ @cweitkamp +/bundles/org.openhab.binding.qbus/ @QbusKoen /bundles/org.openhab.binding.radiothermostat/ @mlobstein /bundles/org.openhab.binding.regoheatpump/ @crnjan /bundles/org.openhab.binding.revogi/ @andibraeu diff --git a/bundles/org.openhab.binding.qbus/README.md b/bundles/org.openhab.binding.qbus/README.md index 4f30c648a4f3e..e8bd53bd75c35 100644 --- a/bundles/org.openhab.binding.qbus/README.md +++ b/bundles/org.openhab.binding.qbus/README.md @@ -2,33 +2,29 @@ ![Qbus Logo](doc/Logo.JPG) -This binding for Qbus communicates for all controllers of the Qbus home automation system. +This binding for [Qbus](https://qbus.be) communicates with all controllers of the Qbus home automation system. -More information about Qbus can be found here: -[Qbus](https://qbus.be) +We also host a site which contains a [manual](https://manualoh.schockaert.tk/) where you can find lot's of information to set up openHAB with Qbus client and server (for the moment only in Dutch). -This is the link to the [Qbus forum](https://qbusforum.be). This forum is mainly in dutch and you can find a lot of information about the pre testings af this binding and offers a way to communicate with other users. - -We also host a site which contains a [manual](https://manualoh.schockaert.tk/) where you can find lot's of information. - -The controllers can not communicate directly with openHAB, therefore we developed a Client/Server application which you must install prior to enable this binding. +The controllers can not communicate directly with openHAB, therefore we developed a client/server application which you must install prior to enable this binding. More information can be found here: -[Qbus Client/Server](https://github.com/QbusKoen/QbusClientServer) +[Qbus Client/Server]( https://github.com/QbusKoen/QbusClientServer-Installer) With this binding you can control and read almost every output from the Qbus system. ## Supported Things -The following things are supported by the Qbus Binding: +The following things are supported by the Qbus binding: -- Dimmer 1 button, 2 button and clc as _dimmer_ -- Bistabiel, Timer1-3, Interval as _onOff_ -- Thermostats - normal and PID as _thermosats_ -- Scenes as _scene_ -- CO2 as _co2_ -- Rollershutter and rollerhutter with slats as _rollershutter_ +- `dimmer`: Dimmer 1 button, 2 button and clc +- `onOff`: Bistabiel, Timer1-3, Interval +- `thermostats`: Thermostats - normal and PID +- `scene`: Scenes +- `co2`: CO2 +- `rollershutter`: Rollershutter +- `rollershutter_slats`: Rollerhutter with slats -For now the folowing Qbus things are not yet supported but will come: +For now the following Qbus things are not yet supported but will come: - DMX - Timer 4 & 5 @@ -60,26 +56,33 @@ Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, refresh rollershutter_slats 121 "Roller2" [ rolId=264 ] } ``` -The Bridge connects to the QbusServer, so if the Client/Server application is installed on the same machine then localhost can be used as address. sn is the serial nr of your controller, port should allways be 8447, except in special installations as it is the communication port of the Server application. refresh is a time in minutes which will check the status of the server and reconnects if connection is broken after this time. + + +| Property | Default | Required | Description | +|------------|----------|----------|----------------| +| `addr` | localhost | YES | The ip address of the machine where the Qbus Server runs | +| `sn` | | YES | The serial number of your controller | +| `port` | 8447 | YES | The communication port of the client/server | +| `refresh` | 5 | NO | Refresh time - After x minutes there will be a check if server is still running and if client is still connected. If not - reconnect | ## Channels -| channel | type | description | +| Thing Type ID | Channel Name | Read only | description | |---------------------|---------------|---------------------------------------------------------| -| onOff | switch | This is the channel for Bistable, Timers and Intervals | -| dimmer | brightness | This is the channel for Dimmers 1&2 buttons and CLC | -| scene | Switch | This is the channel for scenes | -| co2 | co2 | This is the channel for CO2 sensors | -| rollershutter | rollershutter | This is the channel for rollershutters | -| rollershutter_slats | rollershutter | This is the channel for rollershutters with slats | -| thermostat | setpoint | This is the channel for thermostats setpoint | -| thermostat | measured | This is the channel for thermostats currenttemp | -| thermostat | mode | This is the channel for thermostats mode | +| `onOff` | switch | No | This is the channel for Bistable, Timers and Intervals | +| `dimmer` | brightness | No | This is the channel for Dimmers 1&2 buttons and CLC | +| `scene` | Switch | No | This is the channel for scenes | +| `co2` | co2 | Yes | This is the channel for CO2 sensors | +| `rollershutter` | rollershutter | No | This is the channel for rollershutters | +| `rollershutter_slats` | rollershutter | No | This is the channel for rollershutters with slats | +| `thermostat` | setpoint | No | This is the channel for thermostats setpoint | +| `thermostat` | measured | Yes | This is the channel for thermostats currenttemp | +| `thermostat` | mode | No | This is the channel for thermostats mode | ## Full Example -### Things: +### Things ``` Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, refresh=10 ] { @@ -93,13 +96,13 @@ Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, refresh } ``` -### Items: +### Items ``` Dimmer ToonzaalLED [ "Lighting" ] {channel="qbus:dimmer:CTD007841:1:brightness"} Switch Toonzaal230V {channel="qbus:onOff:CTD007841:30:switch"} -Number:Temperature ServiceSP"[%.1f °C]" (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:setpoint"} -Number:Temperature ServiceCT"[%.1f °C]" (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:measured"} +Number:Temperature ServiceSP"[%.1f %unit%]" (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:setpoint"} +Number:Temperature ServiceCT"[%.1f %unit%]" (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:measured"} Number ServiceMode (GroepThermostaten) {channel="qbus:thermostat:CTD007841:50:mode",ihc="0x33c311" , autoupdate="true"} Switch Disco {channel="qbus:scene:CTD007841:36:scene"} Number ProductieCO2 {channel="qbus:co2:CTD007841:100:co2"} @@ -108,3 +111,4 @@ Rollershutter Roller2 {channel Dimmer Roller2_slats {channel="qbus:rollershutter_slats:CTD007841:121:slats"} ``` +This is the link to the [Qbus forum](https://qbusforum.be). This forum is mainly in dutch and you can find a lot of information about the pre testings af this binding and offers a way to communicate with other users. \ No newline at end of file diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java index 5cd8e6bc2b10f..90df9ac47a12e 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java @@ -14,8 +14,6 @@ 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.openhab.core.thing.ThingTypeUID; @@ -37,45 +35,31 @@ public class QbusBindingConstants { // Bridge config properties public static final String CONFIG_HOST_NAME = "addr"; public static final String CONFIG_PORT = "port"; - public static final String CONFIG_REFRESH = "refresh"; public static final String CONFIG_SN = "sn"; // generic thing types public static final ThingTypeUID THING_TYPE_CO2 = new ThingTypeUID(BINDING_ID, "co2"); public static final ThingTypeUID THING_TYPE_SCENE = new ThingTypeUID(BINDING_ID, "scene"); - public static final ThingTypeUID THING_TYPE_TIMER_LIGHT = new ThingTypeUID(BINDING_ID, "onOff"); public static final ThingTypeUID THING_TYPE_ON_OFF_LIGHT = new ThingTypeUID(BINDING_ID, "onOff"); public static final ThingTypeUID THING_TYPE_DIMMABLE_LIGHT = new ThingTypeUID(BINDING_ID, "dimmer"); public static final ThingTypeUID THING_TYPE_ROLLERSHUTTER = new ThingTypeUID(BINDING_ID, "rollershutter"); public static final ThingTypeUID THING_TYPE_ROLLERSHUTTER_SLATS = new ThingTypeUID(BINDING_ID, "rollershutter_slats"); public static final ThingTypeUID THING_TYPE_THERMOSTAT = new ThingTypeUID(BINDING_ID, "thermostat"); - // List of all Thing Type UIDs - public static final Set SCENE_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(THING_TYPE_SCENE).collect(Collectors.toSet())); - - public static final Set CO2_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(THING_TYPE_CO2).collect(Collectors.toSet())); - - public static final Set ROLLERSHUTTER_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(THING_TYPE_ROLLERSHUTTER).collect(Collectors.toSet())); - - public static final Set ROLLERSHUTTER_SLATS_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(THING_TYPE_ROLLERSHUTTER_SLATS).collect(Collectors.toSet())); - public static final Set BISTABIEL_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(THING_TYPE_ON_OFF_LIGHT, THING_TYPE_TIMER_LIGHT).collect(Collectors.toSet())); - - public static final Set THERMOSTAT_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(THING_TYPE_THERMOSTAT).collect(Collectors.toSet())); - - public static final Set DIMMER_THING_TYPES_UIDS = Collections - .unmodifiableSet(Stream.of(THING_TYPE_ON_OFF_LIGHT, THING_TYPE_DIMMABLE_LIGHT).collect(Collectors.toSet())); + // List of all Thing Type UIDs + public static final Set SCENE_THING_TYPES_UIDS = Set.of(THING_TYPE_SCENE); + public static final Set CO2_THING_TYPES_UIDS = Set.of(THING_TYPE_CO2); + public static final Set ROLLERSHUTTER_THING_TYPES_UIDS = Set.of(THING_TYPE_ROLLERSHUTTER); + public static final Set ROLLERSHUTTER_SLATS_THING_TYPES_UIDS = Set.of(THING_TYPE_ROLLERSHUTTER_SLATS); + public static final Set BISTABIEL_THING_TYPES_UIDS = Set.of(THING_TYPE_ON_OFF_LIGHT); + public static final Set THERMOSTAT_THING_TYPES_UIDS = Set.of(THING_TYPE_THERMOSTAT); + public static final Set DIMMER_THING_TYPES_UIDS = Set.of(THING_TYPE_ON_OFF_LIGHT, + THING_TYPE_DIMMABLE_LIGHT); - public static final Set SUPPORTED_THING_TYPES_UIDS = Collections.unmodifiableSet(Stream - .of(THING_TYPE_TIMER_LIGHT, THING_TYPE_ON_OFF_LIGHT, THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_THERMOSTAT, - THING_TYPE_SCENE, THING_TYPE_CO2, THING_TYPE_ROLLERSHUTTER, THING_TYPE_ROLLERSHUTTER_SLATS) - .collect(Collectors.toSet())); + public static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ON_OFF_LIGHT, + THING_TYPE_DIMMABLE_LIGHT, THING_TYPE_THERMOSTAT, THING_TYPE_SCENE, THING_TYPE_CO2, + THING_TYPE_ROLLERSHUTTER, THING_TYPE_ROLLERSHUTTER_SLATS); // List of all Channel ids public static final String CHANNEL_SWITCH = "switch"; diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java index 4377a1fd5803f..44c3d2a8d8dd6 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java @@ -13,8 +13,6 @@ package org.openhab.binding.qbus.internal; -import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; - import java.net.InetAddress; import java.net.UnknownHostException; import java.util.Map; @@ -22,6 +20,8 @@ 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.qbus.internal.protocol.QbusCommunication; import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Bridge; @@ -38,38 +38,49 @@ * * @author Koen Schockaert - Initial Contribution */ + +@NonNullByDefault public class QbusBridgeHandler extends BaseBridgeHandler { private final Logger logger = LoggerFactory.getLogger(QbusBridgeHandler.class); - private QbusCommunication qbusComm; + private @Nullable QbusCommunication qbusComm; + + protected @Nullable QbusConfiguration config; - private ScheduledFuture refreshTimer; + private @Nullable ScheduledFuture refreshTimer; public QbusBridgeHandler(Bridge Bridge) { super(Bridge); } - @Override - public void handleCommand(ChannelUID channelUID, Command command) { - // There is nothing to handle in the bridge handler - } - + /** + * Initialize the bridge + */ @Override public void initialize() { - logger.debug("QBUS: initializing bridge handler"); + logger.info("Initializing bridge handler"); - Configuration config = this.getConfig(); + setConfig(); InetAddress addr = getAddr(); int port = getPort(); - logger.debug("Qbus: bridge handler host {}, port {}", addr, port); - if (addr != null) { createCommunicationObject(addr, port); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - "Qbus: cannot resolve bridge IP with hostname " + config.get(CONFIG_HOST_NAME)); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Cannot connect to QbusServer with hostname " + addr); + } + int refreshInterval = getRefresh(); + this.setupRefreshTimer(refreshInterval); + } + + /** + * Sets the Bridge call back + */ + private void setBridgeCallBack() { + if (qbusComm != null) { + qbusComm.setBridgeCallBack(this); } } @@ -78,34 +89,53 @@ public void initialize() { * * @param addr : IP address of Qbus server * @param port : Communication port of QbusServer - * @param sn : Serial number of Controller */ private void createCommunicationObject(InetAddress addr, int port) { - Configuration config = this.getConfig(); scheduler.submit(() -> { + qbusComm = new QbusCommunication(); - // Set callback from Qbus object to this bridge to be able to take bridge - // offline when non-resolvable communication error occurs. setBridgeCallBack(); - qbusComm.startCommunication(); - if (!qbusComm.communicationActive()) { - qbusComm = null; - bridgeOffline(); - return; + if (qbusComm != null) { + qbusComm.startCommunication(); } - updateStatus(ThingStatus.ONLINE); + if (qbusComm != null) { + if (!qbusComm.communicationActive()) { + qbusComm = null; + bridgeOffline("No communication with Qbus Server"); + return; + } + } - Integer refreshInterval = ((Number) config.get(CONFIG_REFRESH)).intValue(); - setupRefreshTimer(refreshInterval); + if (qbusComm != null) { + if (!qbusComm.clientConnected()) { + + qbusComm = null; + bridgeOffline("No communication with Qbus Client"); + return; + } + } + + updateStatus(ThingStatus.ONLINE); }); } - private void setBridgeCallBack() { - this.qbusComm.setBridgeCallBack(this); + /** + * Take bridge offline when error in communication with Qbus server. This method can also be + * called directly from {@link QbusCommunication} object. + */ + public void bridgeOffline(String message) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); + } + + /** + * Put bridge online when error in communication resolved. + */ + public void bridgeOnline() { + updateStatus(ThingStatus.ONLINE); } /** @@ -113,70 +143,78 @@ private void setBridgeCallBack() { * * @param interval_config Time before refresh in minutes. */ - private void setupRefreshTimer(Integer refreshInterval) { - if (this.refreshTimer != null) { - this.refreshTimer.cancel(true); - this.refreshTimer = null; + + private void setupRefreshTimer(int refreshInterval) { + ScheduledFuture timer = refreshTimer; + if (timer != null) { + timer.cancel(true); + refreshTimer = null; } - if ((refreshInterval == null) || (refreshInterval == 0)) { + if (refreshInterval == 0) { return; } - // This timer will restart the bridge connection periodically - logger.debug("Qbus: Checking for Client communication every {} min", refreshInterval); - this.refreshTimer = scheduler.scheduleWithFixedDelay(() -> { - logger.debug("Qbus: check communication after timerinterval"); - - if (!qbusComm.communicationActive()) { - logger.debug("Qbus: Restarting communication"); - qbusComm.restartCommunication(); - - if (!qbusComm.communicationActive()) { - qbusComm = null; - bridgeOffline(); - updateStatus(ThingStatus.OFFLINE); - return; + // This timer will check connection with server and client periodically + logger.info("Check communication with Server and Client every {} min", refreshInterval); + refreshTimer = scheduler.scheduleWithFixedDelay(() -> { + logger.info("Checking connection with Qbus Server & Client."); + + QbusCommunication comm = getCommunication(); + + if (comm != null) { + if (!comm.communicationActive()) { + // Disconnected from Qbus Server, try to reconnect + logger.info("No connection with Qbus Server. Try to restart."); + comm.restartCommunication(); + if (!comm.communicationActive()) { + bridgeOffline("No connection with Qbus Server"); + return; + } + + } else { + + // Controller disconnected from Qbus client, try to reconnect controller + if (!comm.clientConnected()) { + logger.info("No connection with Qbus Client. Try to restart."); + comm.restartCommunication(); + if (!comm.clientConnected()) { + bridgeOffline("No connection with Qbus Client"); + return; + } + } } - - // updateStatus(ThingStatus.ONLINE); - updateStatus(ThingStatus.ONLINE); - } else { - logger.debug("Qbus: Communication still active"); } + logger.info("Connection with Qbus Server & Client is still active."); + updateStatus(ThingStatus.ONLINE); + }, refreshInterval, refreshInterval, TimeUnit.MINUTES); } /** - * Take bridge offline when error in communication with Qbus server. This method can also be - * called directly from {@link QbusCommunication} object. + * Disposes the Bridge and stops communication with the Qbus server */ - public void bridgeOffline() { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, - "Qbus: error starting bridge connection"); - } - - /** - * Put bridge online when error in communication resolved. - */ - public void bridgeOnline() { - updateStatus(ThingStatus.ONLINE); - } - - @Override - public boolean isInitialized() { - return true; - } - @Override public void dispose() { - if (this.refreshTimer != null) { - this.refreshTimer.cancel(true); + ScheduledFuture timer = refreshTimer; + if (timer != null) { + timer.cancel(true); + } + + refreshTimer = null; + + QbusCommunication comm = getCommunication(); + if (comm != null) { + comm.stopCommunication(); } - this.refreshTimer = null; + + comm = null; } + /** + * Update the configuration parameters + */ @Override public void handleConfigurationUpdate(Map configurationParameters) { Configuration configuration = editConfiguration(); @@ -184,14 +222,14 @@ public void handleConfigurationUpdate(Map configurationParameter configuration.put(configurationParmeter.getKey(), configurationParmeter.getValue()); } updateConfiguration(configuration); + updateStatus(ThingStatus.ONLINE); + } - scheduler.submit(() -> { - - updateStatus(ThingStatus.ONLINE); - - Integer refreshInterval = ((Number) configuration.get(CONFIG_REFRESH)).intValue(); - setupRefreshTimer(refreshInterval); - }); + /** + * Sets the configuration prameters + */ + protected synchronized void setConfig() { + config = getConfig().as(QbusConfiguration.class); } /** @@ -199,7 +237,7 @@ public void handleConfigurationUpdate(Map configurationParameter * * @return Qbus communication object */ - public QbusCommunication getCommunication() { + public @Nullable QbusCommunication getCommunication() { return this.qbusComm; } @@ -208,13 +246,14 @@ public QbusCommunication getCommunication() { * * @return the addr */ - public InetAddress getAddr() { - Configuration config = this.getConfig(); + + @SuppressWarnings("null") + public @Nullable InetAddress getAddr() { InetAddress addr = null; try { - addr = InetAddress.getByName((String) config.get(CONFIG_HOST_NAME)); + addr = InetAddress.getByName(config.addr); } catch (UnknownHostException e) { - logger.debug("Qbus: Cannot resolve hostname {} to IP adress", config.get(CONFIG_HOST_NAME)); + logger.debug("Cannot resolve hostname {} to IP adress", config.addr); } return addr; } @@ -224,18 +263,33 @@ public InetAddress getAddr() { * * @return the port */ + @SuppressWarnings("null") public int getPort() { - Configuration config = this.getConfig(); - return ((Number) config.get(CONFIG_PORT)).intValue(); + return config.port; } /** * Get the serial nr of the Qbus server. * - * @return the sn + * @return the serial nr of the controller */ + @SuppressWarnings("null") public String getSn() { - Configuration config = this.getConfig(); - return ((String) config.get(CONFIG_SN)); + return config.sn; + } + + /** + * Get the refresh interval. + * + * @return the refresh interval + */ + @SuppressWarnings("null") + public int getRefresh() { + return config.refresh; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + // TODO Auto-generated method stub } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java new file mode 100644 index 0000000000000..cd05e2bd585df --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java @@ -0,0 +1,30 @@ +/** + * 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.qbus.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Class {@link QbusConfiguration} Configuration Class + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public class QbusConfiguration { + public String addr = ""; + public int port; + public String sn = ""; + public int refresh; +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java index 5a1cb996c4a91..33d29d2d03253 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java @@ -14,6 +14,8 @@ import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusBistabielHandler; import org.openhab.binding.qbus.internal.handler.QbusCO2Handler; import org.openhab.binding.qbus.internal.handler.QbusDimmerHandler; @@ -36,6 +38,8 @@ */ @Component(service = ThingHandlerFactory.class, configurationPid = "binding.qbus") + +@NonNullByDefault public class QbusHandlerFactory extends BaseThingHandlerFactory { @Override @@ -44,7 +48,7 @@ public boolean supportsThingType(ThingTypeUID thingTypeUID) { } @Override - protected ThingHandler createHandler(Thing thing) { + protected @Nullable ThingHandler createHandler(Thing thing) { if (BRIDGE_THING_TYPES_UIDS.contains(thing.getThingTypeUID())) { QbusBridgeHandler handler = new QbusBridgeHandler((Bridge) thing); return handler; diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java index fcdc07ed2c1ac..8c406b86378f1 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java @@ -12,34 +12,31 @@ */ package org.openhab.binding.qbus.internal.handler; -import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.binding.qbus.internal.QbusBindingConstants.CHANNEL_SWITCH; import static org.openhab.core.types.RefreshType.REFRESH; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.QbusBridgeHandler; import org.openhab.binding.qbus.internal.protocol.QbusBistabiel; import org.openhab.binding.qbus.internal.protocol.QbusCommunication; -import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.OnOffType; -import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * The {@link QbusBistabielHandler} is responsible for handling commands, which are - * sent to one of the channels. + * The {@link QbusBistabielHandler} is responsible for handling the Bistable outputs of Qbus * * @author Koen Schockaert - Initial Contribution */ @NonNullByDefault -public class QbusBistabielHandler extends BaseThingHandler { +public class QbusBistabielHandler extends QbusGlobalHandler { private final Logger logger = LoggerFactory.getLogger(QbusBistabielHandler.class); @@ -47,80 +44,91 @@ public QbusBistabielHandler(Thing thing) { super(thing); } + protected @Nullable QbusThingsConfig config; + + int bistabielId = 0; + + String sn = ""; + + /** + * Main initialization + */ @Override - public void handleCommand(ChannelUID channelUID, Command command) { - Integer BistabielId = ((Number) this.getConfig().get(CONFIG_BISTABIEL_ID)).intValue(); + public void initialize() { - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute bistabiel " + BistabielId); + setConfig(); + bistabielId = getId(); + + QbusCommunication QComm = getCommunication("Bistabiel", bistabielId); + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "No communication with Qbus Bridge!"); return; } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + + QbusBridgeHandler QBridgeHandler = getBridgeHandler("Bistabiel", bistabielId); if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute bistabiel " + BistabielId); return; } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: bridge communication not initialized when trying to execute bistabiel " + BistabielId); - return; - } + QbusBistabiel QBistabiel = QComm.getBistabiel().get(bistabielId); - QbusBistabiel QBistabiel = QComm.getBistabiel().get(BistabielId); + sn = QBridgeHandler.getSn(); if (QBistabiel != null) { - if (QComm.communicationActive()) { - handleCommandSelection(QBistabiel, channelUID, command); - } else { - // We lost connection but the connection object is there, so was correctly started. - // Try to restart communication. - // This can be expensive, therefore do it in a job. - scheduler.submit(() -> { - QComm.restartCommunication(); - // If still not active, take thing offline and return. - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - // Also put the bridge back online - QBridgeHandler.bridgeOnline(); - - // And finally handle the command - handleCommandSelection(QBistabiel, channelUID, command); - }); - } + QBistabiel.setThingHandler(this); + handleStateUpdate(QBistabiel); + logger.info("Bistabiel intialized {}", bistabielId); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: BistabielId " + BistabielId + " does not match a BISTABIEL in the controller"); - return; + + logger.warn("Bistabiel not intialized {}", bistabielId); } } - private void handleCommandSelection(QbusBistabiel QBistabiel, ChannelUID channelUID, Command command) { - logger.debug("Qbus: handle command {} for {}", command, channelUID); + /** + * Handle the status update from the thing + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + QbusCommunication QComm = getCommunication("Bistabiel", bistabielId); + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for bistabiel " + bistabielId); + return; + } + QbusBistabiel QBistabiel = QComm.getBistabiel().get(bistabielId); - if (command == REFRESH) { - handleStateUpdate(QBistabiel); + if (QBistabiel == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for dimmer " + bistabielId); return; } - handleSwitchCommand(QBistabiel, command); - updateStatus(ThingStatus.ONLINE); + scheduler.submit(() -> { + if (!QComm.communicationActive()) { + restartCommunication(QComm, "Bistabiel", bistabielId); + } + + if (QComm.communicationActive()) { + + if (command == REFRESH) { + handleStateUpdate(QBistabiel); + return; + } + + handleSwitchCommand(QBistabiel, channelUID, command); + } + + }); } - private void handleSwitchCommand(QbusBistabiel QBistabiel, Command command) { + /** + * Executes the switch command + */ + private void handleSwitchCommand(QbusBistabiel QBistabiel, ChannelUID channelUID, Command command) { if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; - @SuppressWarnings("null") - String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); - if (s == OnOffType.OFF) { QBistabiel.execute(0, sn); } else { @@ -129,46 +137,6 @@ private void handleSwitchCommand(QbusBistabiel QBistabiel, Command command) { } } - @Override - public void initialize() { - Configuration config = this.getConfig(); - - Integer BistabielId = ((Number) config.get(CONFIG_BISTABIEL_ID)).intValue(); - - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for bistabiel " + BistabielId); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for bistabiel " + BistabielId); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null || !QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: no connection with Qbus server, could not initialize bistabiel " + BistabielId); - return; - } - - QbusBistabiel QBistabiel = QComm.getBistabiel().get(BistabielId); - - // Map properties = new HashMap<>(); - - // thing.setProperties(properties); - - if (QBistabiel != null) { - QBistabiel.setThingHandler(this); - handleStateUpdate(QBistabiel); - logger.info("Qbus: Bistabiel intialized {}", BistabielId); - } else { - logger.info("Qbus: Bistabiel not intialized {} - null", BistabielId); - } - } - /** * Method to update state of channel, called from Qbus Bistabiel. */ @@ -179,4 +147,21 @@ public void handleStateUpdate(QbusBistabiel QBistabiel) { updateState(CHANNEL_SWITCH, (bistabielState == 0) ? OnOffType.OFF : OnOffType.ON); updateStatus(ThingStatus.ONLINE); } + + /** + * Read the configuration + */ + protected synchronized void setConfig() { + config = getConfig().as(QbusThingsConfig.class); + } + + /** + * Returns the Id from the configuration + * + * @return bistabielId + */ + @SuppressWarnings("null") + public int getId() { + return config.bistabielId; + } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java index 26d19930585fb..4bffbc9104885 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java @@ -12,21 +12,18 @@ */ package org.openhab.binding.qbus.internal.handler; -import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.binding.qbus.internal.QbusBindingConstants.CHANNEL_CO2; import static org.openhab.core.types.RefreshType.REFRESH; import org.eclipse.jdt.annotation.NonNullByDefault; import org.openhab.binding.qbus.internal.QbusBridgeHandler; import org.openhab.binding.qbus.internal.protocol.QbusCO2; import org.openhab.binding.qbus.internal.protocol.QbusCommunication; -import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.DecimalType; -import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,8 +34,9 @@ * * @author Koen Schockaert - Initial Contribution */ + @NonNullByDefault -public class QbusCO2Handler extends BaseThingHandler { +public class QbusCO2Handler extends QbusGlobalHandler { private final Logger logger = LoggerFactory.getLogger(QbusCO2Handler.class); @@ -46,120 +44,106 @@ public QbusCO2Handler(Thing thing) { super(thing); } + protected QbusThingsConfig config = getConfig().as(QbusThingsConfig.class);; + + int co2Id = 0; + + String sn = ""; + + /** + * Main initialization + */ @Override - public void handleCommand(ChannelUID channelUID, Command command) { - Integer CO2Id = ((Number) this.getConfig().get(CONFIG_CO2_ID)).intValue(); + public void initialize() { - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute CO2 " + CO2Id); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute CO2 " + CO2Id); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); + setConfig(); + co2Id = getId(); + QbusCommunication QComm = getCommunication("CO2", co2Id); if (QComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: bridge communication not initialized when trying to execute CO2 " + CO2Id); + "No communication with Qbus Bridge!"); return; } - QbusCO2 QCO2 = QComm.getCo2().get(CO2Id); - - if (QCO2 != null) { - if (QComm.communicationActive()) { - handleCommandSelection(QCO2, channelUID, command); - } else { - // We lost connection but the connection object is there, so was correctly started. - // Try to restart communication. - // This can be expensive, therefore do it in a job. - scheduler.submit(() -> { - QComm.restartCommunication(); - // If still not active, take thing offline and return. - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - // Also put the bridge back online - QBridgeHandler.bridgeOnline(); - - // And finally handle the command - handleCommandSelection(QCO2, channelUID, command); - }); - } - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: CO2Id " + CO2Id + " does not match a CO2 in the controller"); + QbusBridgeHandler QBridgeHandler = getBridgeHandler("CO2", co2Id); + if (QBridgeHandler == null) { return; } - } - private void handleCommandSelection(QbusCO2 QCO2, ChannelUID channelUID, Command command) { - logger.debug("Qbus: handle command {} for {}", command, channelUID); + QbusCO2 QCo2 = QComm.getCo2().get(co2Id); - if (command == REFRESH) { - handleStateUpdate(QCO2); - return; - } + sn = QBridgeHandler.getSn(); - updateStatus(ThingStatus.ONLINE); + if (QCo2 != null) { + QCo2.setThingHandler(this); + handleStateUpdate(QCo2); + logger.info("CO2 intialized {}", co2Id); + } else { + logger.info("CO2 not intialized {}", co2Id); + } } + /** + * Handle the status update from the thing + */ @Override - public void initialize() { - Configuration config = this.getConfig(); - - Integer CO2Id = ((Number) config.get(CONFIG_CO2_ID)).intValue(); - - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for CO2 " + CO2Id); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for CO2 " + CO2Id); + public void handleCommand(ChannelUID channelUID, Command command) { + QbusCommunication QComm = getCommunication("CO2", co2Id); + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for CO2 " + co2Id); return; } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null || !QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: no connection with Qbus server, could not initialize CO2 " + CO2Id); + + QbusCO2 QCo2 = QComm.getCo2().get(co2Id); + + if (QCo2 == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for CO2 " + co2Id); return; } - QbusCO2 QCO2 = QComm.getCo2().get(CO2Id); - - // Map properties = new HashMap<>(); + scheduler.submit(() -> { + if (!QComm.communicationActive()) { + restartCommunication(QComm, "CO2", co2Id); + } - // thing.setProperties(properties); + if (QComm.communicationActive()) { + if (command == REFRESH) { + handleStateUpdate(QCo2); + return; + } - if (QCO2 != null) { - QCO2.setThingHandler(this); - handleStateUpdate(QCO2); - logger.info("Qbus: CO2 intialized {}", CO2Id); - } else { - logger.info("Qbus: CO2 not intialized {} - null", CO2Id); - } + } + }); } /** * Method to update state of channel, called from Qbus CO2. */ - public void handleStateUpdate(QbusCO2 QCO2) { + public void handleStateUpdate(QbusCO2 QCo2) { - int CO2State = QCO2.getState(); + int CO2State = QCo2.getState(); updateState(CHANNEL_CO2, new DecimalType(CO2State)); updateStatus(ThingStatus.ONLINE); } + + /** + * Read the configuration + */ + protected synchronized void setConfig() { + config = getConfig().as(QbusThingsConfig.class); + } + + /** + * Returns the Id from the configuration + * + * @return co2Id + */ + + public int getId() { + return config.co2Id; + } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java index 584bf7a004e8a..5a6ad5d981bad 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java @@ -16,31 +16,29 @@ import static org.openhab.core.types.RefreshType.REFRESH; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.QbusBridgeHandler; import org.openhab.binding.qbus.internal.protocol.QbusCommunication; import org.openhab.binding.qbus.internal.protocol.QbusDimmer; -import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.OnOffType; import org.openhab.core.library.types.PercentType; -import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** - * The {@link QbusDimmerHandler} is responsible for handling commands, which are - * sent to one of the channels. + * The {@link QbusDimmerHandler} is responsible for handling the dimmable outputs of Qbus * * @author Koen Schockaert - Initial Contribution */ + @NonNullByDefault -public class QbusDimmerHandler extends BaseThingHandler { +public class QbusDimmerHandler extends QbusGlobalHandler { private final Logger logger = LoggerFactory.getLogger(QbusDimmerHandler.class); @@ -48,91 +46,101 @@ public QbusDimmerHandler(Thing thing) { super(thing); } + protected @Nullable QbusThingsConfig config; + + int dimmerId = 0; + + String sn = ""; + + /** + * Main initialisation + */ @Override - public void handleCommand(ChannelUID channelUID, Command command) { - Integer dimmerId = ((Number) this.getConfig().get(CONFIG_DIMMER_ID)).intValue(); + public void initialize() { + setConfig(); + dimmerId = getId(); - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute dimmer " + dimmerId); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute dimmer " + dimmerId); + QbusCommunication QComm = getCommunication("Dimmer", dimmerId); + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "No communication with Qbus Bridge!"); return; } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: bridge communication not initialized when trying to execute dimmer " + dimmerId); + QbusBridgeHandler QBridgeHandler = getBridgeHandler("Dimmer", dimmerId); + if (QBridgeHandler == null) { return; } QbusDimmer QDimmer = QComm.getDimmer().get(dimmerId); + sn = QBridgeHandler.getSn(); + if (QDimmer != null) { - if (QComm.communicationActive()) { - handleCommandSelection(QDimmer, channelUID, command); - } else { - // We lost connection but the connection object is there, so was correctly started. - // Try to restart communication. - // This can be expensive, therefore do it in a job. - scheduler.submit(() -> { - QComm.restartCommunication(); - // If still not active, take thing offline and return. - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - // Also put the bridge back online - QBridgeHandler.bridgeOnline(); - - // And finally handle the command - handleCommandSelection(QDimmer, channelUID, command); - }); - } + QDimmer.setThingHandler(this); + handleStateUpdate(QDimmer); + logger.info("Dimmer intialized {}", dimmerId); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: dimmerId " + dimmerId + " does not match a DIMMER in the controller"); - return; + logger.warn("Dimmer not intialized {}", dimmerId); } } - private void handleCommandSelection(QbusDimmer QDimmer, ChannelUID channelUID, Command command) { - logger.debug("Qbus: handle command {} for {}", command, channelUID); + /** + * Handle the status update from the thing + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + QbusCommunication QComm = getCommunication("Dimmer", dimmerId); - if (command == REFRESH) { - handleStateUpdate(QDimmer); + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for dimmer " + dimmerId); return; } - switch (channelUID.getId()) { - case CHANNEL_SWITCH: - handleSwitchCommand(QDimmer, command); - updateStatus(ThingStatus.ONLINE); - break; - - case CHANNEL_BRIGHTNESS: - handleBrightnessCommand(QDimmer, command); - updateStatus(ThingStatus.ONLINE); - break; + QbusDimmer QDimmer = QComm.getDimmer().get(dimmerId); - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: channel unknown " + channelUID.getId()); + if (QDimmer == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for dimmer " + dimmerId); + return; } - } - private void handleSwitchCommand(QbusDimmer QDimmer, Command command) { + scheduler.submit(() -> { + if (!QComm.communicationActive()) { + restartCommunication(QComm, "Dimmer", dimmerId); + } + + if (QComm.communicationActive()) { + + if (command == REFRESH) { + handleStateUpdate(QDimmer); + return; + } + + switch (channelUID.getId()) { + case CHANNEL_SWITCH: + handleSwitchCommand(QDimmer, command); + updateStatus(ThingStatus.ONLINE); + break; - @SuppressWarnings("null") - String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + case CHANNEL_BRIGHTNESS: + handleBrightnessCommand(QDimmer, command); + updateStatus(ThingStatus.ONLINE); + break; + default: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Channel unknown " + channelUID.getId()); + } + } + }); + } + + /** + * Executes the switch command + */ + private void handleSwitchCommand(QbusDimmer QDimmer, Command command) { if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; if (s == OnOffType.OFF) { @@ -143,11 +151,10 @@ private void handleSwitchCommand(QbusDimmer QDimmer, Command command) { } } + /** + * Executes the brightness command + */ private void handleBrightnessCommand(QbusDimmer QDimmer, Command command) { - // Bridge QBridge = getBridge(); - @SuppressWarnings("null") - String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); - if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; if (s == OnOffType.OFF) { @@ -181,46 +188,6 @@ private void handleBrightnessCommand(QbusDimmer QDimmer, Command command) { } } - @Override - public void initialize() { - Configuration config = this.getConfig(); - - Integer dimmerId = ((Number) config.get(CONFIG_DIMMER_ID)).intValue(); - - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for dimmer " + dimmerId); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for dimmer " + dimmerId); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null || !QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: no connection with Qbus, could not initialize dimmer " + dimmerId); - return; - } - - QbusDimmer QDimmer = QComm.getDimmer().get(dimmerId); - - // Map properties = new HashMap<>(); - - // thing.setProperties(properties); - - if (QDimmer != null) { - QDimmer.setThingHandler(this); - handleStateUpdate(QDimmer); - logger.info("Qbus: Dimmer intialized {}", dimmerId); - } else { - logger.info("Qbus: Dimmer not intialized {} - null", dimmerId); - } - } - /** * Method to update state of channel, called from Qbus Dimmer. */ @@ -232,4 +199,21 @@ public void handleStateUpdate(QbusDimmer QDimmer) { updateStatus(ThingStatus.ONLINE); } + + /** + * Read the configuration + */ + protected synchronized void setConfig() { + config = getConfig().as(QbusThingsConfig.class); + } + + /** + * Returns the Id from the configuration + * + * @return dimmerId + */ + @SuppressWarnings("null") + public int getId() { + return config.dimmerId; + } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java new file mode 100644 index 0000000000000..48a05c95341ec --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.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.qbus.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.binding.qbus.internal.protocol.QbusCommunication; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; + +/** + * The {@link QbusGlobalHandler} is used in other handlers, to share the functions. + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public abstract class QbusGlobalHandler extends BaseThingHandler { + + public QbusGlobalHandler(Thing thing) { + super(thing); + } + + /** + * Get Bridge communication + * + * @param type + * @param globalId + * @return + */ + public @Nullable QbusCommunication getCommunication(String type, int globalId) { + QbusBridgeHandler QBridgeHandler = getBridgeHandler(type, globalId); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "No bridge handler initialized for " + type + " with id " + globalId + "."); + return null; + } + QbusCommunication QComm = QBridgeHandler.getCommunication(); + return QComm; + } + + /** + * Get the Bridge handler + * + * @param type + * @param globalId + * @return + */ + public @Nullable QbusBridgeHandler getBridgeHandler(String type, int globalId) { + Bridge QBridge = getBridge(); + if (QBridge == null) { + updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + "No bridge initialized for " + type + " with ID " + globalId); + return null; + } + QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + return QBridgeHandler; + } + + /** + * + * @param QComm + * @param type + * @param globalId + */ + public void restartCommunication(QbusCommunication QComm, String type, int globalId) { + QComm.restartCommunication(); + + if (!QComm.communicationActive()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Communication socket error"); + return; + } + + QbusBridgeHandler QBridgeHandler = getBridgeHandler(type, globalId); + if (QBridgeHandler != null) { + QBridgeHandler.bridgeOnline(); + } + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java index 694909b8f6d9d..bf8fcb67d26df 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java @@ -16,18 +16,16 @@ import static org.openhab.core.types.RefreshType.REFRESH; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.QbusBridgeHandler; import org.openhab.binding.qbus.internal.protocol.QbusCommunication; import org.openhab.binding.qbus.internal.protocol.QbusRol; -import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.PercentType; -import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,8 +36,9 @@ * * @author Koen Schockaert - Initial Contribution */ + @NonNullByDefault -public class QbusRolHandler extends BaseThingHandler { +public class QbusRolHandler extends QbusGlobalHandler { private final Logger logger = LoggerFactory.getLogger(QbusRolHandler.class); @@ -47,89 +46,102 @@ public QbusRolHandler(Thing thing) { super(thing); } + protected @Nullable QbusThingsConfig config; + + int rolId = 0; + + String sn = ""; + + /** + * Main initialization + */ @Override - public void handleCommand(ChannelUID channelUID, Command command) { - Integer RolId = ((Number) this.getConfig().get(CONFIG_ROLLERSHUTTER_ID)).intValue(); + public void initialize() { - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute Slats " + RolId); + setConfig(); + rolId = getId(); + + QbusCommunication QComm = getCommunication("Screen/Store", rolId); + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "No communication with Qbus Bridge!"); return; } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + + QbusBridgeHandler QBridgeHandler = getBridgeHandler("Screen/Store", rolId); if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute Slats " + RolId); return; } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: bridge communication not initialized when trying to execute Slats " + RolId); - return; - } + QbusRol QRol = QComm.getRol().get(rolId); - QbusRol QRol = QComm.getRol().get(RolId); + sn = QBridgeHandler.getSn(); if (QRol != null) { - if (QComm.communicationActive()) { - handleCommandSelection(QRol, channelUID, command); - } else { - // We lost connection but the connection object is there, so was correctly started. - // Try to restart communication. - // This can be expensive, therefore do it in a job. - scheduler.submit(() -> { - QComm.restartCommunication(); - // If still not active, take thing offline and return. - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - // Also put the bridge back online - QBridgeHandler.bridgeOnline(); - - // And finally handle the command - handleCommandSelection(QRol, channelUID, command); - }); - } + QRol.setThingHandler(this); + handleStateUpdate(QRol); + logger.info("Screen/Store intialized {}", rolId); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: RolId " + RolId + " does not match a ROL in the controller"); - return; + logger.warn("Screen/Store not intialized {}", rolId); } } - private void handleCommandSelection(QbusRol qRol, ChannelUID channelUID, Command command) { - logger.debug("Qbus: handle command {} for {}", command, channelUID); + /** + * Handle the status update from the thing + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + QbusCommunication QComm = getCommunication("Screen/Store", rolId); - if (command == REFRESH) { - handleStateUpdate(qRol); + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for Screen/Store " + rolId); return; } - switch (channelUID.getId()) { - case CHANNEL_ROLLERSHUTTER: - handleBrightnessCommand(qRol, command); - updateStatus(ThingStatus.ONLINE); - break; + QbusRol QRol = QComm.getRol().get(rolId); - case CHANNEL_SLATS: - handleSlatsCommand(qRol, command); - updateStatus(ThingStatus.ONLINE); - break; - - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: channel unknown " + channelUID.getId()); + if (QRol == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for ROL " + rolId); + return; } + + scheduler.submit(() -> { + if (!QComm.communicationActive()) { + restartCommunication(QComm, "Screen/Store", rolId); + } + + if (QComm.communicationActive()) { + + if (command == REFRESH) { + handleStateUpdate(QRol); + return; + } + + switch (channelUID.getId()) { + case CHANNEL_ROLLERSHUTTER: + handleScreenposCommand(QRol, command); + updateStatus(ThingStatus.ONLINE); + break; + + case CHANNEL_SLATS: + handleSlatsposCommand(QRol, command); + updateStatus(ThingStatus.ONLINE); + break; + + default: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Channel unknown " + channelUID.getId()); + } + } + }); } - private void handleBrightnessCommand(QbusRol QRol, Command command) { - @SuppressWarnings("null") - String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + /** + * Executes the command for screen up/down position + */ + private void handleScreenposCommand(QbusRol QRol, Command command) { if (command instanceof org.openhab.core.library.types.UpDownType) { org.openhab.core.library.types.UpDownType s = (org.openhab.core.library.types.UpDownType) command; @@ -164,9 +176,10 @@ private void handleBrightnessCommand(QbusRol QRol, Command command) { } } - private void handleSlatsCommand(QbusRol QRol, Command command) { - @SuppressWarnings("null") - String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + /** + * Executes the command for screen slats position + */ + private void handleSlatsposCommand(QbusRol QRol, Command command) { if (command instanceof org.openhab.core.library.types.UpDownType) { org.openhab.core.library.types.UpDownType s = (org.openhab.core.library.types.UpDownType) command; @@ -201,48 +214,8 @@ private void handleSlatsCommand(QbusRol QRol, Command command) { } } - @Override - public void initialize() { - Configuration config = this.getConfig(); - - Integer RolId = ((Number) config.get(CONFIG_ROLLERSHUTTER_ID)).intValue(); - - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for Slats " + RolId); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for Slats " + RolId); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null || !QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: no connection with Qbus server, could not initialize Slats " + RolId); - return; - } - - QbusRol QRol = QComm.getRol().get(RolId); - - // Map properties = new HashMap<>(); - - // thing.setProperties(properties); - - if (QRol != null) { - QRol.setThingHandler(this); - handleStateUpdate(QRol); - logger.info("Qbus: Dimmer intialized {}", RolId); - } else { - logger.info("Qbus: Dimmer not intialized {} - null", RolId); - } - } - /** - * Method to update state of channel, called from Qbus Slats. + * Method to update state of channel, called from Qbus Screen/Store. */ public void handleStateUpdate(QbusRol qRol) { @@ -251,6 +224,24 @@ public void handleStateUpdate(QbusRol qRol) { updateState(CHANNEL_ROLLERSHUTTER, new PercentType(rolState)); updateState(CHANNEL_SLATS, new PercentType(slatState)); + updateStatus(ThingStatus.ONLINE); } + + /** + * Read the configuration + */ + protected synchronized void setConfig() { + config = getConfig().as(QbusThingsConfig.class); + } + + /** + * Returns the Id from the configuration + * + * @return rolId + */ + @SuppressWarnings("null") + public int getId() { + return config.rolId; + } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java index 0a3bccd372920..ba00300fc1001 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java @@ -12,21 +12,19 @@ */ package org.openhab.binding.qbus.internal.handler; -import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.binding.qbus.internal.QbusBindingConstants.CHANNEL_SWITCH; import static org.openhab.core.types.RefreshType.REFRESH; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.QbusBridgeHandler; import org.openhab.binding.qbus.internal.protocol.QbusCommunication; import org.openhab.binding.qbus.internal.protocol.QbusScene; -import org.openhab.core.config.core.Configuration; import org.openhab.core.library.types.OnOffType; -import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -37,8 +35,9 @@ * * @author Koen Schockaert - Initial Contribution */ + @NonNullByDefault -public class QbusSceneHandler extends BaseThingHandler { +public class QbusSceneHandler extends QbusGlobalHandler { private final Logger logger = LoggerFactory.getLogger(QbusSceneHandler.class); @@ -46,76 +45,95 @@ public QbusSceneHandler(Thing thing) { super(thing); } + protected @Nullable QbusThingsConfig config; + + int sceneId = 0; + + String sn = ""; + + /** + * Main initialization + */ @Override - public void handleCommand(ChannelUID channelUID, Command command) { - Integer SceneId = ((Number) this.getConfig().get(CONFIG_SCENE_ID)).intValue(); + public void initialize() { - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute Scene " + SceneId); + setConfig(); + sceneId = getId(); + + QbusCommunication QComm = getCommunication("Scene", sceneId); + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "No communication with Qbus Bridge!"); return; } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + + QbusBridgeHandler QBridgeHandler = getBridgeHandler("Scene", sceneId); if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute Scene " + SceneId); return; } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: bridge communication not initialized when trying to execute scene " + SceneId); - return; - } + QbusScene QScene = QComm.getScenes().get(sceneId); - QbusScene QScene = QComm.getScenes().get(SceneId); + sn = QBridgeHandler.getSn(); if (QScene != null) { - if (QComm.communicationActive()) { - handleCommandSelection(QScene, channelUID, command); - } else { - // We lost connection but the connection object is there, so was correctly started. - // Try to restart communication. - // This can be expensive, therefore do it in a job. - scheduler.submit(() -> { - QComm.restartCommunication(); - // If still not active, take thing offline and return. - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - // Also put the bridge back online - QBridgeHandler.bridgeOnline(); - - // And finally handle the command - handleCommandSelection(QScene, channelUID, command); - }); - } + QScene.setThingHandler(this); + handleStateUpdate(QScene); + logger.info("Scene intialized {}", sceneId); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: SceneId " + SceneId + " does not match a SCENE in the controller"); - return; + logger.warn("Scene not intialized {}", sceneId); } } - private void handleCommandSelection(QbusScene QScene, ChannelUID channelUID, Command command) { - logger.debug("Qbus: handle command {} for {}", command, channelUID); + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + QbusCommunication QComm = getCommunication("Scene", sceneId); - if (command == REFRESH) { - handleStateUpdate(QScene); + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for Scene " + sceneId); return; } - handleSwitchCommand(QScene, command); - updateStatus(ThingStatus.ONLINE); + QbusScene QScene = QComm.getScenes().get(sceneId); + + if (QScene == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for Scene " + sceneId); + return; + } + + scheduler.submit(() -> { + if (!QComm.communicationActive()) { + restartCommunication(QComm, "Scene", sceneId); + } + + if (QComm.communicationActive()) { + + if (command == REFRESH) { + handleStateUpdate(QScene); + return; + } + + switch (channelUID.getId()) { + case CHANNEL_SWITCH: + handleSwitchCommand(QScene, command); + updateStatus(ThingStatus.ONLINE); + break; + + default: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Channel unknown " + channelUID.getId()); + } + } + }); } + /** + * Executes the scene command + */ private void handleSwitchCommand(QbusScene QScene, Command command) { - @SuppressWarnings("null") - String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; if (s == OnOffType.OFF) { @@ -126,50 +144,9 @@ private void handleSwitchCommand(QbusScene QScene, Command command) { } } - @Override - public void initialize() { - Configuration config = this.getConfig(); - - Integer SceneId = ((Number) config.get(CONFIG_SCENE_ID)).intValue(); - - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for scene " + SceneId); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for scene " + SceneId); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null || !QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: no connection with Qbus server, could not initialize scene " + SceneId); - return; - } - - QbusScene QScene = QComm.getScenes().get(SceneId); - - // Map properties = new HashMap<>(); - - // thing.setProperties(properties); - - if (QScene != null) { - QScene.setThingHandler(this); - handleStateUpdate(QScene); - logger.info("Qbus: Scene intialized {}", SceneId); - } else { - logger.info("Qbus: Scene not intialized {} - null", SceneId); - } - } - /** * Method to update state of channel, called from Qbus Scene. */ - public void handleStateUpdate(QbusScene QScene) { int sceneState = QScene.getState(); @@ -177,4 +154,21 @@ public void handleStateUpdate(QbusScene QScene) { updateState(CHANNEL_SWITCH, (sceneState == 0) ? OnOffType.OFF : OnOffType.ON); updateStatus(ThingStatus.ONLINE); } + + /** + * Read the configuration + */ + protected synchronized void setConfig() { + config = getConfig().as(QbusThingsConfig.class); + } + + /** + * Returns the Id from the configuration + * + * @return sceneId + */ + @SuppressWarnings("null") + public int getId() { + return config.sceneId; + } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java index b3663df892800..c03dbcf282c0d 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java @@ -16,21 +16,17 @@ import static org.openhab.core.library.unit.SIUnits.CELSIUS; import static org.openhab.core.types.RefreshType.REFRESH; -import javax.measure.quantity.Temperature; - import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.QbusBridgeHandler; -import org.openhab.binding.qbus.internal.protocol.QThermostat; import org.openhab.binding.qbus.internal.protocol.QbusCommunication; -import org.openhab.core.config.core.Configuration; +import org.openhab.binding.qbus.internal.protocol.QbusThermostat; import org.openhab.core.library.types.DecimalType; import org.openhab.core.library.types.QuantityType; -import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; -import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,8 +37,9 @@ * * @author Koen Schockaert - Initial Contribution */ + @NonNullByDefault -public class QbusThermostatHandler extends BaseThingHandler { +public class QbusThermostatHandler extends QbusGlobalHandler { private final Logger logger = LoggerFactory.getLogger(QbusThermostatHandler.class); @@ -50,146 +47,152 @@ public QbusThermostatHandler(Thing thing) { super(thing); } + protected @Nullable QbusThingsConfig config; + + int thermostatId = 0; + + String sn = ""; + + /** + * Main initialization + */ @Override - public void handleCommand(ChannelUID channelUID, Command command) { - Integer thermostatId = ((Number) this.getConfig().get(CONFIG_THERMOSTAT_ID)).intValue(); + public void initialize() { + setConfig(); + thermostatId = getId(); - Bridge QBridge = getBridge(); - if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute thermostat command " + thermostatId); + QbusCommunication QComm = getCommunication("Thermostat", thermostatId); + if (QComm == null) { return; } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); + + QbusBridgeHandler QBridgeHandler = getBridgeHandler("Thermostat", thermostatId); if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized when trying to execute thermostat command " + thermostatId); return; } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: bridge communication not initialized when trying to execute thermostat command " - + thermostatId); - return; - } + QbusThermostat QThermostat = QComm.getThermostats().get(thermostatId); - QThermostat qThermostat = QComm.getThermostats().get(thermostatId); + sn = QBridgeHandler.getSn(); - if (qThermostat != null) { - if (QComm.communicationActive()) { - handleCommandSelection(qThermostat, channelUID, command); - } else { - scheduler.submit(() -> { - QComm.restartCommunication(); - if (!QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: communication socket error"); - return; - } - QBridgeHandler.bridgeOnline(); - handleCommandSelection(qThermostat, channelUID, command); - }); - } + if (QThermostat != null) { + QThermostat.setThingHandler(this); + handleStateUpdate(QThermostat); + logger.info("Thermostat intialized {}", thermostatId); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Qbus: thermostatId " + thermostatId + " does not match a THERMOSTAT in the controller"); - return; + logger.info("Thermostat not intialized {}", thermostatId); } } - @SuppressWarnings("unchecked") - private void handleCommandSelection(QThermostat qThermostat, ChannelUID channelUID, Command command) { - logger.debug("Qbus: handle command {} for {}", command, channelUID); - @SuppressWarnings("null") - String sn = getBridge().getConfiguration().get(CONFIG_SN).toString(); + /** + * Handle the status update from the thing + */ + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + QbusCommunication QComm = getCommunication("Thermostat", thermostatId); - if (REFRESH.equals(command)) { - handleStateUpdate(qThermostat); + if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for thermostat " + + thermostatId); return; } - switch (channelUID.getId()) { - case CHANNEL_MEASURED: - updateStatus(ThingStatus.ONLINE); - break; + QbusThermostat QThermostat = QComm.getThermostats().get(thermostatId); - case CHANNEL_MODE: - if (command instanceof DecimalType) { - qThermostat.executeMode(((DecimalType) command).intValue(), sn); - } - updateStatus(ThingStatus.ONLINE); - break; + if (QThermostat == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for Scene " + thermostatId); + return; + } - case CHANNEL_SETPOINT: - if (command instanceof QuantityType) { - qThermostat.executeSetpoint(((QuantityType) command).doubleValue(), sn); - } - updateStatus(ThingStatus.ONLINE); + scheduler.submit(() -> { + if (!QComm.communicationActive()) { + restartCommunication(QComm, "Thermostat", thermostatId); + } - break; + if (QComm.communicationActive()) { - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: channel unknown " + channelUID.getId()); - } - } + if (command == REFRESH) { + handleStateUpdate(QThermostat); + return; + } - @Override - public void initialize() { - Configuration config = this.getConfig(); + switch (channelUID.getId()) { + case CHANNEL_MEASURED: + updateStatus(ThingStatus.ONLINE); + break; - Integer thermostatId = ((Number) config.get(CONFIG_THERMOSTAT_ID)).intValue(); + case CHANNEL_MODE: + handleModeCommand(QThermostat, command); + updateStatus(ThingStatus.ONLINE); + break; - Bridge QBrdige = getBridge(); - if (QBrdige == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for thermostat " + thermostatId); - return; - } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBrdige.getHandler(); - if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, - "Qbus: no bridge initialized for thermostat " + thermostatId); - return; - } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - if (QComm == null || !QComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Qbus: no connection with Qbus server, could not initialize thermostat " + thermostatId); - return; - } + case CHANNEL_SETPOINT: + handleSetpointCommand(QThermostat, command); + updateStatus(ThingStatus.ONLINE); + break; + + default: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Channel unknown " + channelUID.getId()); + } + } + }); + } - QThermostat qThermostat = QComm.getThermostats().get(thermostatId); + /** + * Executes the Mode command + */ + private void handleModeCommand(QbusThermostat QThermostat, Command command) { - // Map properties = new HashMap<>(); + if (command instanceof DecimalType) { + QThermostat.executeMode(((DecimalType) command).intValue(), sn); + } + } - // thing.setProperties(properties); + /** + * Executes the Setpoint command + */ + private void handleSetpointCommand(QbusThermostat QThermostat, Command command) { - if (qThermostat != null) { - qThermostat.setThingHandler(this); - handleStateUpdate(qThermostat); - logger.info("Qbus: Thermostat intialized {}", thermostatId); - } else { - logger.info("Qbus: Thermostat not intialized {} - null", thermostatId); + if (command instanceof QuantityType) { + QuantityType s = (QuantityType) command; + QThermostat.executeSetpoint(s.doubleValue(), sn); } } /** * Method to update state of all channels, called from Qbus thermostat. * - * @param qThermostat Qbus thermostat + * @param QThermostat Qbus thermostat * */ - public void handleStateUpdate(QThermostat qThermostat) { + public void handleStateUpdate(QbusThermostat QThermostat) { - updateState(CHANNEL_MEASURED, new QuantityType(qThermostat.getMeasured(), CELSIUS)); + updateState(CHANNEL_MEASURED, new QuantityType<>(QThermostat.getMeasured(), CELSIUS)); - updateState(CHANNEL_SETPOINT, new QuantityType(qThermostat.getSetpoint(), CELSIUS)); + updateState(CHANNEL_SETPOINT, new QuantityType<>(QThermostat.getSetpoint(), CELSIUS)); - updateState(CHANNEL_MODE, new DecimalType(qThermostat.getMode())); + updateState(CHANNEL_MODE, new DecimalType(QThermostat.getMode())); updateStatus(ThingStatus.ONLINE); } + + /** + * Read the configuration + */ + protected synchronized void setConfig() { + config = getConfig().as(QbusThingsConfig.class); + } + + /** + * Returns the Id from the configuration + * + * @return dimmerId + */ + @SuppressWarnings("null") + public int getId() { + return config.thermostatId; + } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java new file mode 100644 index 0000000000000..24b4b2160b77f --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.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.qbus.internal.handler; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * The {@link QbusThingsConfig} is responible for handling configurations for all things + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +public class QbusThingsConfig { + public int bistabielId; + public int dimmerId; + public int co2Id; + public int rolId; + public int sceneId; + public int thermostatId; +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java deleted file mode 100644 index f8d3b27b0e4ab..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageCmd.java +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright (c) 2010-2020 Contributors to the openHAB project - * - * See the NOTICE file(s) distributed with this work for additional - * information. - * - * This program and the accompanying materials are made available under the - * terms of the Eclipse Public License 2.0 which is available at - * http://www.eclipse.org/legal/epl-2.0 - * - * SPDX-License-Identifier: EPL-2.0 - */ -package org.openhab.binding.qbus.internal.protocol; - -/** - * Class {@link QbusMessageCmd} used as input to gson to send commands to Qbus. Extends - * {@link QbusMessageBase}. - *

- * Example: {"cmd":"executebistabiel","id":1,"value1":0} - * - * @author Koen Schockaert - Initial Contribution - */ - -@SuppressWarnings("unused") -class QMessageCmd extends QbusMessageBase { - - private int id; - private Integer pos; - private Integer value1; - private Integer value2; - private Integer mode; - private Double setpoint; - private String ctdsn; - - QMessageCmd(String cmd) { - super.setCmd(cmd); - } - - QMessageCmd(String cmd, int id) { - this(cmd); - this.id = id; - } - - QMessageCmd(String cmd, int id, Integer value1) { - this(cmd, id); - this.value1 = value1; - } - - QMessageCmd(String cmd, int id, Integer value1, Integer value2) { - this(cmd, id, value1); - this.value2 = value2; - } - - QMessageCmd withMode(Integer mode) { - this.mode = mode; - return this; - } - - QMessageCmd withSetpoint(Double d) { - this.setpoint = d; - return this; - } - - QMessageCmd withSn(String sn) { - - this.ctdsn = sn; - return this; - } -} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java index 52547826ee8f6..bf0fca3867252 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java @@ -23,6 +23,7 @@ * * @author Koen Schockaert - Initial Contribution */ + @NonNullByDefault public final class QbusBistabiel { @@ -31,13 +32,13 @@ public final class QbusBistabiel { @Nullable private QbusCommunication QComm; - private int id; + private String id = ""; private Integer state = 0; @Nullable private QbusBistabielHandler thingHandler; - QbusBistabiel(int id) { + QbusBistabiel(String id) { this.id = id; } @@ -79,9 +80,9 @@ public Integer getState() { */ void setState(int state) { this.state = state; - QbusBistabielHandler handler = thingHandler; + QbusBistabielHandler handler = this.thingHandler; if (handler != null) { - logger.debug("Qbus: update bistabiel channel state for {} with {}", id, state); + logger.info("Update bistabiel channel state for {} with {}", id, state); handler.handleStateUpdate(this); } } @@ -91,9 +92,9 @@ void setState(int state) { */ public void execute(int value, String sn) { - logger.debug("Qbus: execute bistabiel for {} on CTD {}", this.id, sn); + logger.info("Execute bistabiel for {}", this.id); - QMessageCmd QCmd = new QMessageCmd("executebistabiel", this.id, value).withSn(sn); + QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeBistabiel").withId(this.id).withState(value); QbusCommunication comm = QComm; if (comm != null) { diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java index b1aa025c16680..e8e8d1ee5c696 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java @@ -24,19 +24,20 @@ * * @author Koen Schockaert - Initial Contribution */ + @NonNullByDefault public final class QbusCO2 { private final Logger logger = LoggerFactory.getLogger(QbusCO2.class); - private int id; - private Integer state = 0; + private String co2Id; + private Integer co2State = 0; @Nullable private QbusCO2Handler thingHandler; - QbusCO2(int id) { - this.id = id; + QbusCO2(String co2Id) { + this.co2Id = co2Id; } /** @@ -56,7 +57,7 @@ public void setThingHandler(QbusCO2Handler handler) { * @return CO2 state */ public Integer getState() { - return this.state; + return this.co2State; } /** @@ -65,10 +66,10 @@ public Integer getState() { * @param CO2 state */ public void setState(Integer co2) { - this.state = co2; + this.co2State = co2; QbusCO2Handler handler = thingHandler; if (handler != null) { - logger.debug("Qbus: update channel state for {} with {}", id, state); + logger.info("Update channel state for {} with {}", co2Id, co2State); handler.handleStateUpdate(this); } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java index dda251ad82894..347c50a32662e 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java @@ -38,7 +38,7 @@ * systems: *

    *
  • Start and stop TCP socket connection with Qbus Server. - *
  • Read all setup and status information from the Qbus Controller. + *
  • Read all the outputs and their status information from the Qbus Controller. *
  • Execute Qbus commands. *
  • Listen to events from Qbus. *
@@ -47,6 +47,7 @@ * * @author Koen Schockaert - Initial Contribution */ + @NonNullByDefault public final class QbusCommunication { @@ -65,13 +66,16 @@ public final class QbusCommunication { private Gson gsonOut = new Gson(); private Gson gsonIn; - private final Map scenes = new HashMap<>(); + private String CTD = ""; + // @Nullable + private Boolean CTDConnected = false; + private final Map bistabiel = new HashMap<>(); + private final Map scene = new HashMap<>(); private final Map dimmer = new HashMap<>(); - private final Map thermostats = new HashMap<>(); + private final Map rol = new HashMap<>(); + private final Map thermostat = new HashMap<>(); private final Map co2 = new HashMap<>(); - private final Map Rol = new HashMap<>(); - // private final Map Disconnect = new HashMap<>(); @Nullable private QbusBridgeHandler bridgeCallBack; @@ -84,7 +88,7 @@ public final class QbusCommunication { public QbusCommunication() { GsonBuilder gsonBuilder = new GsonBuilder(); gsonBuilder.registerTypeAdapter(QbusMessageBase.class, new QbusMessageDeserializer()); - this.gsonIn = gsonBuilder.create(); + gsonIn = gsonBuilder.create(); } /** @@ -93,18 +97,20 @@ public QbusCommunication() { * * @param addr : IP-address of Qbus Server * @param port : Communication port of Qbus server - * @param sn : Serial number of the controller * */ + public synchronized void startCommunication() { - QbusBridgeHandler handler = this.bridgeCallBack; + QbusBridgeHandler handler = bridgeCallBack; try { + for (int i = 1; qEventsRunning && (i <= 5); i++) { Thread.sleep(1000); + } if (qEventsRunning) { - logger.error("Qbus: starting from thread {}, but previous connection still active after 5000ms", + logger.error("Starting from thread {}, but previous connection still active after 5000ms", Thread.currentThread().getId()); throw new IOException(); } @@ -117,22 +123,30 @@ public synchronized void startCommunication() { int port = handler.getPort(); Socket socket = new Socket(addr, port); - this.qSocket = socket; - this.qOut = new PrintWriter(socket.getOutputStream(), true); - this.qIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); - logger.debug("Qbus: connected via local port {} from thread {}", socket.getLocalPort(), + qSocket = socket; + qOut = new PrintWriter(socket.getOutputStream(), true); + qIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); + logger.info("Connected via local port {} from thread {}", socket.getLocalPort(), Thread.currentThread().getId()); + CTDConnected = false; - initialize(); - - (new Thread(qEvents)).start(); + // Connect to Qbus server + Connect(); + // If Qbus Client is connected then initialize, else put Bridge offline + if (CTDConnected == true) { + initialize(); + (new Thread(qEvents)).start(); + } else { + handler.bridgeOffline("No communication with Qbus client"); + } } catch (IOException | InterruptedException e) { - logger.warn("Qbus: error initializing communication from thread {}", Thread.currentThread().getId()); + logger.warn("Error initializing communication from thread {}", Thread.currentThread().getId()); + // No connection with Qbus server, put Bridge offline + stopCommunication(); if (handler != null) { - handler.bridgeOffline(); + handler.bridgeOffline("No communication with Qbus server"); } - stopCommunication(); } } @@ -144,9 +158,9 @@ public synchronized void startCommunication() { * */ public synchronized void stopCommunication() { - this.listenerStopped = true; + listenerStopped = true; - Socket socket = this.qSocket; + Socket socket = qSocket; if (socket != null) { try { @@ -156,25 +170,18 @@ public synchronized void stopCommunication() { } } - this.qSocket = null; - // restartCommunication(); - logger.debug("Qbus: communication stopped from thread {}", Thread.currentThread().getId()); + qSocket = null; + + CTDConnected = false; + logger.debug("Communication stopped from thread {}", Thread.currentThread().getId()); } /** * Close and restart communication with Qbus Server. - * */ public synchronized void restartCommunication() { - QbusBridgeHandler handler = this.bridgeCallBack; stopCommunication(); - // handler.bridgeOffline(); - - if (handler != null) { - handler.bridgeOffline(); - } - logger.debug("Qbus: restart communication from thread {}", Thread.currentThread().getId()); startCommunication(); @@ -186,26 +193,36 @@ public synchronized void restartCommunication() { * @return True if active */ public boolean communicationActive() { - return (this.qSocket != null); + + return (qSocket != null); } /** - * Runnable that handles inbound communication from Qbus server. + * Method to check if communication with Qbus Client is active + * + * @return True if active + */ + + public boolean clientConnected() { + return (CTDConnected); + } + + /** + * Runnable that handles incomming communication from Qbus server. *

* The thread listens to the TCP socket opened at instantiation of the {@link QbusCommunication} class - * and interprets all inbound json messages. It triggers state updates for active channels linked to the + * and interprets all incomming json messages. It triggers state updates for active channels linked to the * Qbus outputs. It is started after initialization of the communication. * */ private Runnable qEvents = () -> { String qMessage; - // QbusBridgeHandler handler = this.bridgeCallBack; - logger.debug("Qbus: listening for events on thread {}", Thread.currentThread().getId()); + logger.info("Listening for events on thread {}", Thread.currentThread().getId()); listenerStopped = false; qEventsRunning = true; - BufferedReader reader = this.qIn; + BufferedReader reader = qIn; try { if (reader == null) { @@ -220,95 +237,145 @@ public boolean communicationActive() { if (!listenerStopped) { qEventsRunning = false; logger.warn("Qbus: IO error in listener on thread {}", Thread.currentThread().getId()); - restartCommunication(); + + QbusBridgeHandler handler = bridgeCallBack; + + if (handler != null) { + CTDConnected = false; + handler.bridgeOffline("No communication with Qbus server"); + } + return; } } qEventsRunning = false; - stopCommunication(); - - logger.debug("Qbus: event listener thread stopped on thread {}", Thread.currentThread().getId()); + logger.warn("Event listener thread stopped on thread {}", Thread.currentThread().getId()); }; + /** + * Called by other methods to send json data to Qbus. + * + * @param qMessage + */ + synchronized void sendMessage(Object qMessage) { + PrintWriter writer = qOut; + String json = gsonOut.toJson(qMessage); + logger.debug("Send json from thread {}", Thread.currentThread().getId()); + + if (writer != null) { + writer.println(json); + // Delay after sending data to improve scene execution + try { + TimeUnit.MILLISECONDS.sleep(250); + } catch (InterruptedException e) { + // No reaction on error is required + } + + } + if ((writer == null) || (writer.checkError())) { + logger.warn("Error sending message, trying to restart communication"); + restartCommunication(); + // retry sending after restart + logger.debug("Resend json from thread {}", Thread.currentThread().getId()); + writer = qOut; + if (writer != null) { + writer.println(json); + } + if ((writer == null) || (writer.checkError())) { + logger.warn("Error resending message"); + + } + } + } + + /** + * Called by other methods to Qbus server and read response + */ + private void sendAndReadMessage(String command) throws IOException, InterruptedException { + QbusMessageCmd qCmd = new QbusMessageCmd(CTD, command); + + sendMessage(qCmd); + + BufferedReader reader = qIn; + if (reader == null) { + throw new IOException("Cannot read from socket, reader not connected."); + } + readMessage(reader.readLine()); + } + /** * Method that interprets all feedback from Qbus Server application and calls appropriate handling methods. * * @param qMessage message read from Qbus. */ + // @SuppressWarnings("null") private void readMessage(String qMessage) { - logger.debug("Qbus: received json on thread {}", Thread.currentThread().getId()); - - try { - QbusMessageBase qMessageGson = this.gsonIn.fromJson(qMessage, QbusMessageBase.class); - String cmd = ""; - String event = ""; - - @SuppressWarnings("null") - String confsn = this.bridgeCallBack.getSn(); - @SuppressWarnings("null") - String sn = qMessageGson.getSn(); + logger.debug("Received json on thread {}", Thread.currentThread().getId()); + String confsn = ""; + String cmd = ""; + String CTD = ""; + QbusMessageBase qMessageGson; + + qMessageGson = gsonIn.fromJson(qMessage, QbusMessageBase.class); + if (qMessageGson != null) { + CTD = qMessageGson.getSn(); cmd = qMessageGson.getCmd(); - event = qMessageGson.getEvent(); + } - if (Integer.parseInt(confsn) == Integer.parseInt(sn)) { + if (bridgeCallBack != null) { + confsn = bridgeCallBack.getSn(); + } + + try { + if (Integer.parseInt(confsn) == Integer.parseInt(CTD) && qMessageGson != null) { // Get the compatible outputs from the Qbus server - if ("listbistabiel".equals(cmd)) { - cmdListBistabiel(((QMessageListMap) qMessageGson).getData()); - } else if ("listdimmers".equals(cmd)) { - cmdListDimmers(((QMessageListMap) qMessageGson).getData()); - } else if (("listthermostats").equals(cmd)) { - cmdListThermostat(((QMessageListMap) qMessageGson).getData()); - } else if (("listscenes").equals(cmd)) { - cmdlistscenes(((QMessageListMap) qMessageGson).getData()); - } else if (("listco2").equals(cmd)) { - cmdlistco2(((QMessageListMap) qMessageGson).getData()); - } else if (("listrol").equals(cmd)) { - cmdlistrol(((QMessageListMap) qMessageGson).getData()); - } else if (("listrol02pslats").equals(cmd)) { - cmdlistrolslats(((QMessageListMap) qMessageGson).getData()); + if ("returnBistabiel".equals(cmd)) { + cmdListBistabiel(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if ("returnDimmer".equals(cmd)) { + cmdListDimmers(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if (("returnThermostat").equals(cmd)) { + cmdListThermostat(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if (("returnScene").equals(cmd)) { + cmdlistscenes(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if (("returnCo2").equals(cmd)) { + cmdlistco2(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if (("returnRol02p").equals(cmd)) { + cmdlistrol(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if (("returnSlat").equals(cmd)) { + cmdlistrolslats(((QbusMessageListMap) qMessageGson).getOutputs()); } - // Commands to execute from openHAB to Qbus - else if ("executebistabiel".equals(cmd)) { - cmdExecuteBistabiel(((QMessageMap) qMessageGson).getData()); - } else if ("executedimmers".equals(cmd)) { - cmdExecuteDimmer(((QMessageMap) qMessageGson).getData()); - } else if ("executethermostat".equals(cmd)) { - cmdExecuteThermostat(((QMessageMap) qMessageGson).getData()); - } else if ("executescene".equals(cmd)) { - cmdExecuteScene(((QMessageMap) qMessageGson).getData()); - } else if ("executeslats".equals(cmd)) { - cmdExecuteSlats(((QMessageMap) qMessageGson).getData()); - } else if ("executerol".equals(cmd)) { - cmdExecuteRol(((QMessageMap) qMessageGson).getData()); - } else if ("executerol02pslats".equals(cmd)) { - cmdExecuteRolslats(((QMessageMap) qMessageGson).getData()); - } - // Incoming commands from Qbus Server to openHAB (event) - else if ("listbistabiel".equals(event)) { - eventListBistabiel(((QMessageListMap) qMessageGson).getData()); - } else if ("listdimmers".equals(event)) { - eventListDimmers(((QMessageListMap) qMessageGson).getData()); - } else if ("listthermostat".equals(event)) { - eventListThermostat(((QMessageListMap) qMessageGson).getData()); - } else if ("listscenes".equals(event)) { - eventListScenes(((QMessageListMap) qMessageGson).getData()); - } else if ("listco2".equals(event)) { - eventListCO2(((QMessageListMap) qMessageGson).getData()); - } else if ("listrol".equals(event)) { - eventListRol(((QMessageListMap) qMessageGson).getData()); - } else if ("listrol02pslats".equals(event)) { - eventListRolslats(((QMessageListMap) qMessageGson).getData()); + + // Incoming commands from Qbus Client to openHAB (event) + else if ("updateBistabiel".equals(cmd)) { + updateBistabiel(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if ("updateDimmer".equals(cmd)) { + eventListDimmers(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if ("updateThermostat".equals(cmd)) { + eventListThermostat(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if ("updateScene".equals(cmd)) { + eventListScenes(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if ("updateCo2".equals(cmd)) { + eventListCO2(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if ("updateRol02p".equals(cmd)) { + eventListRol(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if ("updateRol02pSlat".equals(cmd)) { + eventListRolslats(((QbusMessageListMap) qMessageGson).getOutputs()); } - // - else if ("disconnect".equals(event)) { - eventFunction(((QMessageListMap) qMessageGson).getData()); + + // Incomming commands from Qbus server to verify the client connection + else if ("disconnect".equals(cmd)) { + eventDisconnect(); + } else if ("notConnected".equals(cmd)) { + noConnection(); + } else if ("connected".equals(cmd)) { + connection(); } } } catch (JsonParseException e) { - logger.debug("Qbus: not acted on unsupported json {}", qMessage); + logger.warn("Not acted on unsupported json {}", qMessage); } } @@ -316,51 +383,68 @@ else if ("disconnect".equals(event)) { * After setting up the communication with the Qbus Server, send all initialization messages. *

* First send connect to connect with the Qbus Server application - * Get request for the Scenes * Get request for Bistabiel/Timers/Intervals/Mono outputs + * Get request for the Scenes * Get request for Dimmers 1T and 2T + * Get request for Shutters * Get request for Thermostats * Get request for CO2 - * Get request for Shutters + * * * @throws IOException * @throws InterruptedException */ + private void initialize() throws IOException, InterruptedException { - Connect(); - sendAndReadMessage("listrol"); - sendAndReadMessage("listrol02pslats"); - sendAndReadMessage("listscenes"); - sendAndReadMessage("listbistabiel"); - sendAndReadMessage("listdimmers"); - sendAndReadMessage("listthermostats"); - sendAndReadMessage("listco2"); + if (bridgeCallBack != null) { + if (CTDConnected) { + logger.info("Requesting Bistabiel outputs from client"); + sendAndReadMessage("getBistabiel"); + logger.info("Requesting Scenes from client"); + sendAndReadMessage("getScene"); + logger.info("Requesting Dimmers from client"); + sendAndReadMessage("getDimmer"); + logger.info("Requesting Shutters from client"); + sendAndReadMessage("getRol02p"); + logger.info("Requesting Shutters whith slat control from client"); + sendAndReadMessage("getRol02pSlat"); + logger.info("Requesting Thermostats from client"); + sendAndReadMessage("getThermostat"); + logger.info("Requesting CO2 outputs from client"); + sendAndReadMessage("getCo2"); + } else { + logger.warn("No CTD client connected to server with sn {}", CTD); + CTDConnected = false; + + QbusBridgeHandler handler = bridgeCallBack; + + if (handler != null) { + handler.bridgeOffline("No communication with Qbus client"); + } + + return; + } + } else { + logger.error("Initialization error"); + } } /** * Initial connection to Qbus Server to open a communication channel * - * @throws InterruptedException - */ - private void Connect() throws InterruptedException { - @SuppressWarnings("null") - String confsn = this.bridgeCallBack.getSn(); - QMessageCmd qCmd = new QMessageCmd("openHAB").withSn(confsn); - sendMessage(qCmd); - TimeUnit.MILLISECONDS.sleep(250); - } - - /** - * Send message to Qbus server and read response + * @throws IOException + * */ - private void sendAndReadMessage(String command) throws IOException, InterruptedException { - @SuppressWarnings("null") - String confsn = this.bridgeCallBack.getSn(); - QMessageCmd qCmd = new QMessageCmd(command).withSn(confsn); + private void Connect() throws InterruptedException, IOException { + if (bridgeCallBack != null) { + CTD = bridgeCallBack.getSn(); + } + logger.info("Connecting to server"); + QbusMessageCmd QCmd = new QbusMessageCmd(CTD, "openHAB"); - sendMessage(qCmd); + sendMessage(QCmd); + BufferedReader reader = qIn; - BufferedReader reader = this.qIn; if (reader == null) { throw new IOException("Cannot read from socket, reader not connected."); } @@ -368,394 +452,305 @@ private void sendAndReadMessage(String command) throws IOException, InterruptedE } /** - * Get all the scenes from the Qbus server + * Get all the Bistabiel/Timer/Mono/Interval outputs from the Qbus client * * @param data */ - private void cmdlistscenes(@Nullable List> data) { - logger.debug("Qbus: Scenes received from Qbus server"); + @SuppressWarnings("null") + private void cmdListBistabiel(@Nullable List> outputs) { + logger.info("Bistabiel/Timers/Monos/Intervals received from Qbus server"); - if (data != null) { - for (Map scene : data) { - String idStr = scene.get("id"); - if (idStr != null) { - try { - int id = Integer.parseInt(idStr); - QbusScene Scene = new QbusScene(id); - Scene.setQComm(this); - this.scenes.put(id, Scene); - } catch (Exception e) { - logger.debug("Qbus: Error in json for Scenes"); + if (outputs != null) { + for (Map bistabiel : outputs) { + String idStr = bistabiel.get("id"); + String stateStr = bistabiel.get("state"); + + if (idStr != null && stateStr != null) { + int id = Integer.parseInt(idStr); + Integer state = Integer.parseInt(stateStr); + if (!this.bistabiel.containsKey(id)) { + QbusBistabiel qBistabiel = new QbusBistabiel(idStr); + qBistabiel.setState(state); + qBistabiel.setQComm(this); + this.bistabiel.put(id, qBistabiel); + this.bistabiel.get(id).setState(state); + } else { + this.bistabiel.get(id).setState(state); } } else { - logger.debug("Qbus: Error in json for Scenes Nullvalue"); + logger.error("Error in json for BistabBistabiel/Timers/Monos/Intervals"); } } } } /** - * Get all the CO2 outputs from the Qbus server + * Get all the scenes from the Qbus server * - * @param data + * @param outputs */ - @SuppressWarnings("null") - private void cmdlistco2(@Nullable List> data) { - logger.debug("Qbus: CO2 received from Qbus server"); - - if (data != null) { - for (Map co2 : data) { - String idStr = co2.get("id"); - String value1Str = co2.get("value1"); - if (idStr != null && value1Str != null) { + private void cmdlistscenes(@Nullable List> outputs) { + logger.info("Scenes received from Qbus server"); + if (outputs != null) { + for (Map scene : outputs) { + String idStr = scene.get("id"); + if (idStr != null) { int id = Integer.parseInt(idStr); - Integer co = Integer.parseInt(value1Str); - - if (!this.co2.containsKey(id)) { - QbusCO2 CO2 = new QbusCO2(id); - this.co2.put(id, CO2); - this.co2.get(id).setState(co); - } else { - this.co2.get(id).setState(co); - } - + QbusScene Scene = new QbusScene(idStr); + Scene.setQComm(this); + this.scene.put(id, Scene); } else { - logger.debug("Qbus: Error in json for CO2 Nullvalue"); + logger.error("Error in json for Scenes"); } - } } } /** - * Get all the Positioning module outputs from the Qbus server + * Get all the Dimmer outputs from the Qbus client * - * @param data + * @param outputs */ @SuppressWarnings("null") - private void cmdlistrol(@Nullable List> data) { - logger.debug("Qbus: ROL02P received from Qbus server"); + private void cmdListDimmers(@Nullable List> outputs) { + logger.info("Dimmers received from the Qbus server"); - if (data != null) { - for (Map rol : data) { - String idStr = rol.get("id"); - String value1Str = rol.get("value1"); - if (idStr != null && value1Str != null) { + if (outputs != null) { + for (Map dimmer : outputs) { + String idStr = dimmer.get("id"); + String stateStr = dimmer.get("state"); + if (idStr != null && stateStr != null) { int id = Integer.parseInt(idStr); - Integer rolpos = Integer.valueOf(value1Str); - if (!this.Rol.containsKey(id)) { - QbusRol Rol = new QbusRol(id); - Rol.setQComm(this); - this.Rol.put(id, Rol); - this.Rol.get(id).setState(rolpos); + Integer state = Integer.parseInt(stateStr); + if (!this.dimmer.containsKey(id)) { + QbusDimmer qDimmer = new QbusDimmer(idStr); + qDimmer.updateState(state); + qDimmer.setQComm(this); + this.dimmer.put(id, qDimmer); } else { - this.Rol.get(id).setState(rolpos); + this.dimmer.get(id).updateState(state); } } else { - logger.debug("Qbus: Error in json for ROL02P Nullvalue"); + logger.error("Error in json for Dimmer"); } } } } /** - * Get all the Positioning module outputs from the Qbus server + * Get all the screens with slat control outputs from the Qbus client * * @param data */ @SuppressWarnings("null") - private void cmdlistrolslats(@Nullable List> data) { - logger.debug("Qbus: ROL02PSLATS received from Qbus server"); - - if (data != null) { - for (Map rol : data) { + private void cmdlistrol(@Nullable List> outputs) { + logger.info("ROL02P received from Qbus server"); + if (outputs != null) { + for (Map rol : outputs) { String idStr = rol.get("id"); - String value1Str = rol.get("value1"); - String value2Str = rol.get("value2"); - if (idStr != null && value1Str != null && value2Str != null) { + String stateStr = rol.get("state"); + if (idStr != null && stateStr != null) { int id = Integer.parseInt(idStr); - Integer rolpos = Integer.valueOf(value1Str); - Integer rolposslats = Integer.valueOf(value2Str); - rolpos = Integer.valueOf(rolpos); - rolposslats = Integer.valueOf(rolposslats); - if (!this.Rol.containsKey(id)) { - QbusRol Rol = new QbusRol(id); + Integer rolpos = Integer.valueOf(stateStr); + if (!this.rol.containsKey(id)) { + QbusRol Rol = new QbusRol(idStr); Rol.setQComm(this); - this.Rol.put(id, Rol); - this.Rol.get(id).setState(rolpos); - this.Rol.get(id).setSlats(rolposslats); + this.rol.put(id, Rol); + this.rol.get(id).setState(rolpos); } else { - this.Rol.get(id).setState(rolpos); - this.Rol.get(id).setSlats(rolposslats); + this.rol.get(id).setState(rolpos); } } else { - logger.debug("Qbus: Error in json for ROL02P_Slats Nullvalue"); + logger.error("Error in json for ROL02P"); } } } } /** - * Get all the Bistabiel/Timers/Mono/Interval from the Qbus server + * Get all the screen outputs from the Qbus client * * @param data */ @SuppressWarnings("null") - private void cmdListBistabiel(@Nullable List> data) { - logger.debug("Qbus: Bistabiel/Timers/Monos/Intervals received from Qbus server"); + private void cmdlistrolslats(@Nullable List> outputs) { + logger.info("ROL02PSLATS received from Qbus server"); - if (data != null) { - for (Map bistabiel : data) { - String idStr = bistabiel.get("id"); - String value1Str = bistabiel.get("value1"); - if (idStr != null && value1Str != null) { + if (outputs != null) { + for (Map rol : outputs) { + + String idStr = rol.get("id"); + String rolPos = rol.get("rolPos"); + String slatPos = rol.get("slatPos"); + if (idStr != null && rolPos != null && slatPos != null) { int id = Integer.parseInt(idStr); - Integer state = Integer.parseInt(value1Str); - if (!this.bistabiel.containsKey(id)) { - QbusBistabiel qBistabiel = new QbusBistabiel(id); - qBistabiel.setState(state); - qBistabiel.setQComm(this); - this.bistabiel.put(id, qBistabiel); + Integer rolpos = Integer.valueOf(rolPos); + Integer rolposslats = Integer.valueOf(slatPos); + rolpos = Integer.valueOf(rolpos); + rolposslats = Integer.valueOf(rolposslats); + if (!this.rol.containsKey(id)) { + QbusRol Rol = new QbusRol(idStr); + Rol.setQComm(this); + this.rol.put(id, Rol); + this.rol.get(id).setState(rolpos); + this.rol.get(id).setSlats(rolposslats); } else { - this.bistabiel.get(id).setState(state); + this.rol.get(id).setState(rolpos); + this.rol.get(id).setSlats(rolposslats); } + } else { + logger.error("Error in json for ROL02P_Slats"); } } } } /** - * Get all the Dimmer outputs from the Qbus server + * Get all the CO2 outputs from the Qbus client * * @param data */ @SuppressWarnings("null") - private void cmdListDimmers(@Nullable List> data) { - logger.debug("Qbus: Dimmers received from the Qbus server"); + private void cmdlistco2(@Nullable List> outputs) { + logger.info("CO2 received from Qbus server"); + + if (outputs != null) { + for (Map co2 : outputs) { + String idStr = co2.get("id"); + String stateStr = co2.get("state"); + if (idStr != null && stateStr != null) { - if (data != null) { - for (Map dimmer : data) { - String idStr = dimmer.get("id"); - String value1Str = dimmer.get("value1"); - if (idStr != null && value1Str != null) { int id = Integer.parseInt(idStr); - Integer state = Integer.parseInt(value1Str); - if (!this.dimmer.containsKey(id)) { - QbusDimmer qDimmer = new QbusDimmer(id); - qDimmer.updateState(state); - qDimmer.setQComm(this); - this.dimmer.put(id, qDimmer); + int state = Integer.parseInt(stateStr); + + if (!this.co2.containsKey(id)) { + QbusCO2 CO2 = new QbusCO2(idStr); + this.co2.put(id, CO2); + this.co2.get(id).setState(state); } else { - this.dimmer.get(id).updateState(state); + this.co2.get(id).setState(state); } + + } else { + logger.error("Error in json for CO2"); } + } } } /** - * Get all the Thermostat outputs from the Qbus server + * Get all the Thermostat outputs from the Qbus client * * @param data */ @SuppressWarnings("null") - private void cmdListThermostat(@Nullable List> data) { - logger.debug("Qbus: thermostats received from the Qbus server"); + private void cmdListThermostat(@Nullable List> outputs) { + logger.info("Thermostats received from the Qbus server"); - if (data != null) { - for (Map thermostat : data) { + if (outputs != null) { + for (Map thermostat : outputs) { String idStr = thermostat.get("id"); String measuredStr = thermostat.get("measured"); - String setpointStr = thermostat.get("setpoint"); - String modeStr = thermostat.get("mode"); + String setpointStr = thermostat.get("SetPoint"); + String modeStr = thermostat.get("Mode"); if (idStr != null && measuredStr != null && setpointStr != null && modeStr != null) { int id = Integer.parseInt(idStr); Double measured = Double.valueOf(measuredStr); Double setpoint = Double.valueOf(setpointStr); Integer mode = Integer.valueOf(modeStr); - if (!this.thermostats.containsKey(id)) { - QThermostat qThermostat = new QThermostat(id); + if (!this.thermostat.containsKey(id)) { + QbusThermostat qThermostat = new QbusThermostat(idStr); qThermostat.updateState(measured, setpoint, mode); qThermostat.setQComm(this); - this.thermostats.put(id, qThermostat); + this.thermostat.put(id, qThermostat); } else { - this.thermostats.get(id).updateState(measured, setpoint, mode); + this.thermostat.get(id).updateState(measured, setpoint, mode); } + } else { + logger.error("Error in json for Thermostats"); } } } } /** - * Execute Bistabiel/Timers/Monos/Intervals - * - * @param data - */ - private void cmdExecuteBistabiel(Map data) { - String errorCodeStr = data.get("error"); - if (errorCodeStr != null) { - Integer errorCode = Integer.valueOf(errorCodeStr); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute bistabiel success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); - } - } - } - - /** - * Execute Scenes - * - * @param data - */ - private void cmdExecuteScene(Map data) { - String errorCodeStr = data.get("error"); - if (errorCodeStr != null) { - Integer errorCode = Integer.valueOf(errorCodeStr); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute scene success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); - } - } - } - - /** - * Execute Dimmers + * Event on incoming Bistabiel/Timer/Mono/Interval updates * * @param data */ - private void cmdExecuteDimmer(Map data) { - String errorCodeStr = data.get("error"); - if (errorCodeStr != null) { - Integer errorCode = Integer.valueOf(errorCodeStr); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute dimmer success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); - } - } - } - - /** - * Execute Slats - * - * @param data - */ - private void cmdExecuteSlats(Map data) { - String errorCodeStr = data.get("error"); - if (errorCodeStr != null) { - Integer errorCode = Integer.valueOf(errorCodeStr); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute slats success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); - } - } - } - - /** - * Execute Shutter - * - * @param data - */ - private void cmdExecuteRol(Map data) { - String errorCodeStr = data.get("error"); - if (errorCodeStr != null) { - Integer errorCode = Integer.valueOf(errorCodeStr); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute shutter success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); - } - } - } - - /** - * Execute Shutter with slats - * - * @param data - */ - private void cmdExecuteRolslats(Map data) { - String errorCodeStr = data.get("error"); - if (errorCodeStr != null) { - Integer errorCode = Integer.valueOf(errorCodeStr); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute shutter with slats success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); - } - } - } - - /** - * Execute Thermostats - * - * @param data - */ - private void cmdExecuteThermostat(Map data) { - String errorCodeStr = data.get("error"); - if (errorCodeStr != null) { - Integer errorCode = Integer.valueOf(errorCodeStr); - if (errorCode.equals(0)) { - logger.debug("Qbus: execute thermostat success"); - } else { - logger.warn("Qbus: error code {} returned on command execution", errorCode); + @SuppressWarnings("null") + private void updateBistabiel(List> output) { + for (Map bistabiel : output) { + String idStr = bistabiel.get("id"); + String stateStr = bistabiel.get("state"); + if (idStr != null && stateStr != null) { + int id = Integer.valueOf(idStr); + int value1 = Integer.valueOf(stateStr); + if (!this.bistabiel.containsKey(id)) { + logger.warn("Bistabiel in controller not known {}", id); + return; + } + logger.info("Event execute bistabiel {} with state {}", id, value1); + this.bistabiel.get(id).setState(value1); } } } /** - * Event on incomming Bistabiel/Timer/Mono/Interval updates + * Event on incoming Scene updates * * @param data */ @SuppressWarnings("null") - private void eventListBistabiel(List> data) { - for (Map bistabiel : data) { - String idStr = bistabiel.get("id"); - String value1Str = bistabiel.get("value1Str"); + private void eventListScenes(List> data) { + for (Map scene : data) { + String idStr = scene.get("id"); + String value1Str = scene.get("state"); if (idStr != null && value1Str != null) { int id = Integer.valueOf(idStr); int value1 = Integer.valueOf(value1Str); - if (!this.bistabiel.containsKey(id)) { - logger.warn("Qbus: bistabiel in controller not known {}", id); + if (!this.scene.containsKey(id)) { + logger.warn("Scene in controller not known {}", id); return; } - logger.debug("Qbus: event execute bistabiel {} with state {}", id, value1); - this.bistabiel.get(id).setState(value1); + + logger.info("Event execute scene {} with state {}", id, value1); + this.scene.get(id).setState(value1); } } } /** - * Event on incomming CO2 updates + * Event on incoming Dimmer updates * * @param data */ @SuppressWarnings("null") - private void eventListCO2(List> data) { - for (Map co2 : data) { - String idStr = co2.get("id"); - String value1Str = co2.get("value1Str"); + private void eventListDimmers(List> data) { + for (Map dimmer : data) { + String idStr = dimmer.get("id"); + String value1Str = dimmer.get("state"); if (idStr != null && value1Str != null) { int id = Integer.valueOf(idStr); int value1 = Integer.valueOf(value1Str); - if (!this.co2.containsKey(id)) { - logger.warn("Qbus: co2 in controller not known {}", id); + + if (!this.dimmer.containsKey(id)) { + logger.warn("Dimmer in controller not known {}", id); return; } - logger.debug("Qbus: event execute co2 {} with state {}", id, value1); - this.co2.get(id).setState(value1); + + logger.info("Event execute dimmer {} with state {}", id, value1); + this.dimmer.get(id).setState(value1); } } } /** - * Event on incomming ROL02P without updates + * Event on incoming screen updates * * @param data */ @@ -769,19 +764,19 @@ private void eventListRol(List> data) { int id = Integer.valueOf(idStr); int value1 = Integer.valueOf(value1Str); - if (!this.Rol.containsKey(id)) { - logger.warn("Qbus: Rol02p in controller not known {}", id); + if (!this.rol.containsKey(id)) { + logger.warn("Rol02p in controller not known {}", id); return; } - logger.debug("Qbus: event execute Rol02P {} with pos {}", id, value1); - this.Rol.get(id).setState(value1); + logger.info("Event execute Rol02P {} with pos {}", id, value1); + this.rol.get(id).setState(value1); } } } /** - * Event on incomming ROL02P with slats updates + * Event on incoming screen with slats updates * * @param data */ @@ -795,69 +790,20 @@ private void eventListRolslats(List> data) { int id = Integer.valueOf(idStr); int value1 = Integer.valueOf(value1Str); int value2 = Integer.valueOf(value2Str); - if (!this.Rol.containsKey(id)) { - logger.warn("Qbus: Rol02p in controller not known {}", id); - return; - } - - logger.debug("Qbus: event execute ROL02P_Slats {} with pos {} and slats {}", id, value1, value2); - this.Rol.get(id).setState(value1); - this.Rol.get(id).setSlats(value2); - } - } - } - - /** - * Event on incomming Scene updates - * - * @param data - */ - @SuppressWarnings("null") - private void eventListScenes(List> data) { - for (Map scene : data) { - String idStr = scene.get("id"); - String value1Str = scene.get("value1"); - if (idStr != null && value1Str != null) { - int id = Integer.valueOf(idStr); - int value1 = Integer.valueOf(value1Str); - if (!this.scenes.containsKey(id)) { - logger.warn("Qbus: scene in controller not known {}", id); + if (!this.rol.containsKey(id)) { + logger.warn("Rol02p in controller not known {}", id); return; } - logger.debug("Qbus: event execute scene {} with state {}", id, value1); - this.scenes.get(id).setState(value1); + logger.info("Event execute ROL02P_Slats {} with pos {} and slats {}", id, value1, value2); + this.rol.get(id).setState(value1); + this.rol.get(id).setSlats(value2); } } } /** - * Event on incomming Dimmer updates - * - * @param data - */ - @SuppressWarnings("null") - private void eventListDimmers(List> data) { - for (Map dimmer : data) { - String idStr = dimmer.get("id"); - String value1Str = dimmer.get("value1"); - if (idStr != null && value1Str != null) { - int id = Integer.valueOf(idStr); - int value1 = Integer.valueOf(value1Str); - - if (!this.dimmer.containsKey(id)) { - logger.warn("Qbus: dimmer in controller not known {}", id); - return; - } - - logger.debug("Qbus: event execute dimmer {} with state {}", id, value1); - this.dimmer.get(id).setState(value1); - } - } - } - - /** - * Event on incomming thermostat updates + * Event on incoming thermostat updates * * @param data */ @@ -874,73 +820,62 @@ private void eventListThermostat(List> data) { Double setpoint = Double.valueOf(setpointdStr); Integer mode = Integer.valueOf(modedStr); - if (!this.thermostats.containsKey(id)) { - logger.warn("Qbus: thermostat in controller not known {}", id); + if (!this.thermostat.containsKey(id)) { + logger.warn("Thermostat in controller not known {}", id); return; } - logger.debug("Qbus: event execute thermostat {} with measured {}, setpoint {}, mode {}", id, measured, + logger.info("Event execute thermostat {} with measured {}, setpoint {}, mode {}", id, measured, setpoint, mode); - this.thermostats.get(id).updateState(measured, setpoint, mode); + this.thermostat.get(id).updateState(measured, setpoint, mode); } } } - /* - * /** - * Event on Disconnect + /** + * Event on incoming CO2 updates * * @param data */ - private void eventFunction(List> data) { - // for (Map function : data) { - logger.debug("Disconnect"); - // String ctd = function.get("ctd"); - // String funcion = function.get("function"); - - QbusBridgeHandler handler = this.bridgeCallBack; + @SuppressWarnings("null") + private void eventListCO2(List> data) { + for (Map co2 : data) { + String idStr = co2.get("id"); + String value1Str = co2.get("value1Str"); + if (idStr != null && value1Str != null) { + int id = Integer.valueOf(idStr); + int value1 = Integer.valueOf(value1Str); + if (!this.co2.containsKey(id)) { + logger.warn("Co2 in controller not known {}", id); + return; + } + logger.info("Event execute co2 {} with state {}", id, value1); + this.co2.get(id).setState(value1); + } + } + } + private void eventDisconnect() { + logger.info("Disconnect received from client. Putting Bridge offline"); + CTDConnected = false; + QbusBridgeHandler handler = bridgeCallBack; if (handler != null) { - stopCommunication(); - handler.bridgeOffline(); - + handler.bridgeOffline("No communication with Qbus client"); } } - /** - * Called by other methods to send json cmd to Qbus. - * - * @param qMessage - */ - synchronized void sendMessage(Object qMessage) { - PrintWriter writer = this.qOut; - String json = gsonOut.toJson(qMessage); - logger.debug("Qbus: send json from thread {}", Thread.currentThread().getId()); - - if (writer != null) { - writer.println(json); - - try { - TimeUnit.MILLISECONDS.sleep(250); - } catch (InterruptedException e) { - // No reaction on error is required - } - + private void noConnection() { + logger.info("No CTD connected to Qbus server"); + CTDConnected = false; + QbusBridgeHandler handler = bridgeCallBack; + if (handler != null) { + handler.bridgeOffline("No communication with Qbus client"); } - if ((writer == null) || (writer.checkError())) { - logger.warn("Qbus: error sending message, trying to restart communication"); - restartCommunication(); - // retry sending after restart - logger.debug("Qbus: resend json from thread {}", Thread.currentThread().getId()); - writer = this.qOut; - if (writer != null) { - writer.println(json); - } - if ((writer == null) || (writer.checkError())) { - logger.warn("Qbus: error resending message"); + } - } - } + private void connection() { + logger.info("CTD connected to Qbus server"); + CTDConnected = true; } /** @@ -952,6 +887,15 @@ public Map getBistabiel() { return this.bistabiel; } + /** + * Return all Scenes in the Qbus Controller + * + * @return + */ + public Map getScenes() { + return this.scene; + } + /** * Return all Dimmers in the Qbus Controller. * @@ -962,12 +906,12 @@ public Map getDimmer() { } /** - * Return all Scenes in the Qbus Controller + * Return all screen outûts in the Qbus Controller. * * @return */ - public Map getScenes() { - return this.scenes; + public Map getRol() { + return this.rol; } /** @@ -975,8 +919,8 @@ public Map getScenes() { * * @return */ - public Map getThermostats() { - return this.thermostats; + public Map getThermostats() { + return this.thermostat; } /** @@ -988,15 +932,6 @@ public Map getCo2() { return this.co2; } - /** - * Return all ROL02P outûts in the Qbus Controller. - * - * @return - */ - public Map getRol() { - return this.Rol; - } - /** * @param bridgeCallBack the bridgeCallBack to set */ diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java index d852491dc7704..d2cdb3c674090 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java @@ -23,6 +23,7 @@ * * @author Koen Schockaert - Initial Contribution */ + @NonNullByDefault public final class QbusDimmer { @@ -31,13 +32,13 @@ public final class QbusDimmer { @Nullable private QbusCommunication QComm; - private int id; + private String id = ""; private Integer state = 0; @Nullable private QbusDimmerHandler thingHandler; - QbusDimmer(int id) { + QbusDimmer(String id) { this.id = id; } @@ -51,7 +52,7 @@ public void updateState(Integer state) { QbusDimmerHandler handler = thingHandler; if (handler != null) { - logger.debug("Qbus: update channels for {}", id); + logger.info("Qbus: update channels for {}", id); handler.handleStateUpdate(this); } } @@ -96,7 +97,7 @@ void setState(int state) { this.state = state; QbusDimmerHandler handler = thingHandler; if (handler != null) { - logger.debug("Qbus: update channel state for {} with {}", id, state); + logger.info("Update channel state for {} with {}", id, state); handler.handleStateUpdate(this); } } @@ -105,9 +106,9 @@ void setState(int state) { * Sends Dimmer state to Qbus. */ public void execute(int percent, String sn) { - logger.debug("Qbus: execute dimmer for {} on CTD {}", this.id, sn); + logger.info("Execute dimmer for {} ", this.id); - QMessageCmd QCmd = new QMessageCmd("executedimmer", this.id, percent).withSn(sn); + QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeDimmer").withId(this.id).withState(percent); QbusCommunication comm = QComm; if (comm != null) { diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java index 6b6b1bf71900a..5bc33a2c4aed5 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java @@ -24,9 +24,21 @@ abstract class QbusMessageBase { - private String cmd = ""; - private String event1 = ""; - private String sn = ""; + private String CTD; + protected String cmd; + protected String id; + protected Integer state; + protected Integer mode; + protected Double setpoint; + protected Integer slatState; + + String getSn() { + return this.CTD; + } + + void setSn(String CTD) { + this.CTD = CTD; + } String getCmd() { return this.cmd; @@ -36,19 +48,43 @@ void setCmd(String cmd) { this.cmd = cmd; } - void setSn(String sn) { - this.sn = sn; + public String getId() { + return id; } - String getEvent() { - return this.event1; + public void setId(String id) { + this.id = id; } - void setEvent(String event) { - this.event1 = event; + public int getState() { + return state; } - String getSn() { - return this.sn; + public void setState(int state) { + this.state = state; + } + + public int getMode() { + return mode; + } + + public void setMode(int mode) { + this.mode = mode; + } + + public Double getSetPoint() { + return setpoint; + } + + public void setSetPoint(Double setpoint) { + this.setpoint = setpoint; + } + + public int getSlatState() { + return slatState; + } + + public void setSlatState(int slatState) { + this.slatState = slatState; } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java new file mode 100644 index 0000000000000..34a4140c257c3 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.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.qbus.internal.protocol; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * Class {@link QbusMessageCmd} used as input to gson to send commands to Qbus. Extends + * {@link QbusMessageBase}. + *

+ * Example: {"cmd":"executebistabiel","id":1,"value1":0} + * + * @author Koen Schockaert - Initial Contribution + */ + +@NonNullByDefault +class QbusMessageCmd extends QbusMessageBase { + + QbusMessageCmd(String CTD) { + super.setSn(CTD); + } + + QbusMessageCmd(String CTD, String cmd) { + this(CTD); + this.cmd = cmd; + } + + QbusMessageCmd withId(String id) { + this.setId(id); + return this; + } + + QbusMessageCmd withState(int state) { + this.setState(state); + return this; + } + + QbusMessageCmd withMode(int mode) { + this.setMode(mode); + return this; + } + + QbusMessageCmd withSetPoint(Double setpoint) { + this.setSetPoint(setpoint); + return this; + } + + QbusMessageCmd withSlatState(int slatState) { + this.setSlatState(slatState); + return this; + } +} diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java index 73f76050aa548..5fb2b9a88cbe8 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.Map.Entry; +import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import com.google.gson.JsonArray; @@ -36,6 +37,7 @@ * */ +@NonNullByDefault class QbusMessageDeserializer implements JsonDeserializer { @Override @@ -46,59 +48,56 @@ class QbusMessageDeserializer implements JsonDeserializer { try { String cmd = null; - String event1 = null; - String sn = null; + String CTD = null; if (jsonObject.has("cmd")) { cmd = jsonObject.get("cmd").getAsString(); } - if (jsonObject.has("event1")) { - event1 = jsonObject.get("event1").getAsString(); - } - if (jsonObject.has("ctdsn")) { - sn = jsonObject.get("ctdsn").getAsString(); + + if (jsonObject.has("CTD")) { + CTD = jsonObject.get("CTD").getAsString(); } - JsonElement jsonData = null; - if (jsonObject.has("data")) { - jsonData = jsonObject.get("data"); + JsonElement jsonOutputs = null; + + if (jsonObject.has("outputs")) { + jsonOutputs = jsonObject.get("outputs"); } QbusMessageBase message = null; - if (jsonData != null) { - if (jsonData.isJsonObject()) { - message = new QMessageMap(); + if (jsonOutputs != null) { + if (jsonOutputs.isJsonObject()) { + message = new QbusMessageMap(); - Map data = new HashMap<>(); - for (Entry entry : jsonData.getAsJsonObject().entrySet()) { - data.put(entry.getKey(), entry.getValue().getAsString()); + Map outputs = new HashMap<>(); + for (Entry entry : jsonOutputs.getAsJsonObject().entrySet()) { + outputs.put(entry.getKey(), entry.getValue().getAsString()); } - ((QMessageMap) message).setData(data); + ((QbusMessageMap) message).setOutputs(outputs); - } else if (jsonData.isJsonArray()) { - JsonArray jsonDataArray = jsonData.getAsJsonArray(); + } else if (jsonOutputs.isJsonArray()) { + JsonArray jsonOutputsArray = jsonOutputs.getAsJsonArray(); - message = new QMessageListMap(); + message = new QbusMessageListMap(); - List> dataList = new ArrayList<>(); - for (int i = 0; i < jsonDataArray.size(); i++) { - JsonObject jsonDataObject = jsonDataArray.get(i).getAsJsonObject(); + List> outputsList = new ArrayList<>(); + for (int i = 0; i < jsonOutputsArray.size(); i++) { + JsonObject jsonOutputsObject = jsonOutputsArray.get(i).getAsJsonObject(); - Map data = new HashMap<>(); - for (Entry entry : jsonDataObject.entrySet()) { - data.put(entry.getKey(), entry.getValue().getAsString()); + Map outputs = new HashMap<>(); + for (Entry entry : jsonOutputsObject.entrySet()) { + outputs.put(entry.getKey(), entry.getValue().getAsString()); } - dataList.add(data); + outputsList.add(outputs); } - ((QMessageListMap) message).setData(dataList); + ((QbusMessageListMap) message).setOutputs(outputsList); } } - if (message != null) { + if (message != null && cmd != null && CTD != null) { message.setCmd(cmd); - message.setEvent(event1); - message.setSn(sn); + message.setSn(CTD); } else { throw new JsonParseException("Unexpected Json type"); } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageListMap.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageListMap.java similarity index 69% rename from bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageListMap.java rename to bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageListMap.java index 1bc73610537a0..66a30947eb08e 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageListMap.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageListMap.java @@ -16,6 +16,8 @@ import java.util.List; import java.util.Map; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Class {@link QbusMessageListMap} used as output from gson for cmd or event feedback from Qbus where the * data part is enclosed by [] and contains a list of json strings. Extends {@link QbusMessageBase}. @@ -24,15 +26,16 @@ * @author Koen Schockaert - Initial Contribution */ -class QMessageListMap extends QbusMessageBase { +@NonNullByDefault +class QbusMessageListMap extends QbusMessageBase { - private List> data = new ArrayList<>(); + private List> outputs = new ArrayList<>(); - List> getData() { - return this.data; + List> getOutputs() { + return this.outputs; } - void setData(List> data) { - this.data = data; + void setOutputs(List> outputs) { + this.outputs = outputs; } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageMap.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageMap.java similarity index 72% rename from bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageMap.java rename to bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageMap.java index 139ecee65bd47..f061119b1ebb9 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QMessageMap.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageMap.java @@ -15,6 +15,8 @@ import java.util.HashMap; import java.util.Map; +import org.eclipse.jdt.annotation.NonNullByDefault; + /** * Class {@link QbusMessageMap} used as output from gson for cmd or event feedback from Qbus where the * data part is a simple json string. Extends {@link QbusMessageBase}. @@ -22,15 +24,17 @@ * * @author Koen Schockaert - Initial Contribution */ -class QMessageMap extends QbusMessageBase { - private Map data = new HashMap<>(); +@NonNullByDefault +class QbusMessageMap extends QbusMessageBase { + + private Map outputs = new HashMap<>(); Map getData() { - return this.data; + return this.outputs; } - void setData(Map data) { - this.data = data; + void setOutputs(Map outputs) { + this.outputs = outputs; } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java index 29d4cb6779c41..9933f643996e0 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java @@ -23,6 +23,7 @@ * * @author Koen Schockaert - Initial Contribution */ + @NonNullByDefault public final class QbusRol { @@ -31,14 +32,14 @@ public final class QbusRol { @Nullable private QbusCommunication QComm; - private int id; + private String id = ""; private Integer state = 0; private Integer slats = 0; @Nullable private QbusRolHandler thingHandler; - QbusRol(int id) { + QbusRol(String id) { this.id = id; } @@ -92,7 +93,7 @@ public void setState(Integer Slats) { this.state = Slats; QbusRolHandler handler = thingHandler; if (handler != null) { - logger.debug("Qbus: update channel shutter for {} with {}", id, state); + logger.info("Update channel shutter for {} with {}", id, state); handler.handleStateUpdate(this); } } @@ -106,7 +107,7 @@ public void setSlats(Integer Slats) { this.slats = Slats; QbusRolHandler handler = thingHandler; if (handler != null) { - logger.debug("Qbus: update channel slats for {} with {}", id, slats); + logger.info("Update channel slats for {} with {}", id, slats); handler.handleStateUpdate(this); } } @@ -114,10 +115,10 @@ public void setSlats(Integer Slats) { /** * Sends shutter to Qbus. */ - public void execute(int percent, String sn) { - logger.debug("Qbus: execute position for {} {}", this.id, sn); + public void execute(int value, String sn) { + logger.info("Execute position for {}", this.id); - QMessageCmd QCmd = new QMessageCmd("executestore", this.id, percent).withSn(sn); + QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeStore").withId(this.id).withState(value); QbusCommunication comm = QComm; if (comm != null) { @@ -128,10 +129,10 @@ public void execute(int percent, String sn) { /** * Sends slats to Qbus. */ - public void executeSlats(int percent, String sn) { - logger.debug("Qbus: execute slats for {} {}", this.id, sn); + public void executeSlats(int value, String sn) { + logger.info("Execute slats for {}", this.id); - QMessageCmd QCmd = new QMessageCmd("executeslats", this.id, percent).withSn(sn); + QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeStore").withId(this.id).withSlatState(value); QbusCommunication comm = QComm; if (comm != null) { diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java index f97c7eb82fa87..e1cfa4f0a0697 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java @@ -23,6 +23,7 @@ * * @author Koen Schockaert - Initial Contribution */ + @NonNullByDefault public final class QbusScene { @@ -31,13 +32,13 @@ public final class QbusScene { @Nullable private QbusCommunication QComm; - private int id = 0; + private String id = ""; private Integer state = 0; @Nullable private QbusSceneHandler thingHandler; - QbusScene(int id) { + QbusScene(String id) { this.id = id; } @@ -81,7 +82,7 @@ void setState(int state) { this.state = state; QbusSceneHandler handler = thingHandler; if (handler != null) { - logger.debug("Qbus: update channel state for {} with {}", id, state); + logger.info("Update channel state for {} with {}", id, state); handler.handleStateUpdate(this); } } @@ -89,11 +90,10 @@ void setState(int state) { /** * Sends action to Qbus. */ - public void execute(int val, String sn) { - logger.debug("Qbus: execute scene for {} on CTD {}", this.id, sn); + public void execute(int value, String sn) { + logger.info("Execute scene for {} ", this.id); - QMessageCmd QCmd = new QMessageCmd("executescene", this.id, val).withSn(sn); - ; + QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeScene").withId(this.id).withState(value); QbusCommunication comm = QComm; if (comm != null) { diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QThermostat.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java similarity index 84% rename from bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QThermostat.java rename to bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java index 4663194c57fa8..68da81b61352e 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QThermostat.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java @@ -19,21 +19,22 @@ import org.slf4j.LoggerFactory; /** - * The {@link QThermostat} class represents the thermostat Qbus communication object. It contains all + * The {@link QbusThermostat} class represents the thermostat Qbus communication object. It contains all * fields representing a Qbus thermostat and has methods to set the thermostat mode and setpoint in Qbus and * receive thermostat updates. * * @author Koen Schockaert - Initial Contribution */ + @NonNullByDefault -public final class QThermostat { +public final class QbusThermostat { - private final Logger logger = LoggerFactory.getLogger(QThermostat.class); + private final Logger logger = LoggerFactory.getLogger(QbusThermostat.class); @Nullable private QbusCommunication qComm; - private int id; + private String id; private Double measured = 0.0; private Double setpoint = 0.0; private Integer mode = 0; @@ -41,7 +42,7 @@ public final class QThermostat { @Nullable private QbusThermostatHandler thingHandler; - QThermostat(int id) { + QbusThermostat(String id) { this.id = id; } @@ -59,7 +60,7 @@ public void updateState(Double measured, Double setpoint, Integer mode) { QbusThermostatHandler handler = thingHandler; if (handler != null) { - logger.debug("Qbus: update channels for {}", id); + logger.info("Update channels for {}", id); handler.handleStateUpdate(this); } } @@ -107,7 +108,7 @@ private void setMeasured(Double measured) { /** * Get setpoint * - * @return the setpoint temperature in 1°C multiples + * @return the setpoint temperature in 0.5°C multiples */ public double getSetpoint() { return this.setpoint; @@ -147,9 +148,9 @@ private void setMode(Integer mode) { * @param sn */ public void executeMode(int mode, String sn) { - logger.debug("Qbus: execute thermostat mode {} for {} on {}", mode, this.id, sn); + logger.info("Execute thermostat mode {} for {}", mode, id); - QMessageCmd qCmd = new QMessageCmd("executethermostat", this.id).withMode(mode).withSn(sn); + QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeThermostat").withId(this.id).withMode(mode); QbusCommunication comm = qComm; if (comm != null) { @@ -163,9 +164,9 @@ public void executeMode(int mode, String sn) { * @param d */ public void executeSetpoint(double d, String sn) { - logger.debug("Qbus: execute thermostat setpoint {} for {} on {}", d, this.id, sn); + logger.info("Execute thermostat setpoint {} for {} ", d, id); - QMessageCmd qCmd = new QMessageCmd("executethermostat", this.id).withSetpoint(d).withSn(sn); + QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeThermostat").withId(this.id).withSetPoint(setpoint); QbusCommunication comm = qComm; if (comm != null) { From ed74a2b0f8e8e17af297dd6c0baf76f329ac2746 Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Sat, 9 Jan 2021 13:08:04 +0100 Subject: [PATCH 06/17] Update README.md Signed-off-by: QbusKoen --- bundles/org.openhab.binding.qbus/README.md | 55 ++++++++++------------ 1 file changed, 25 insertions(+), 30 deletions(-) diff --git a/bundles/org.openhab.binding.qbus/README.md b/bundles/org.openhab.binding.qbus/README.md index e8bd53bd75c35..3565e02b4b36b 100644 --- a/bundles/org.openhab.binding.qbus/README.md +++ b/bundles/org.openhab.binding.qbus/README.md @@ -4,11 +4,11 @@ This binding for [Qbus](https://qbus.be) communicates with all controllers of the Qbus home automation system. -We also host a site which contains a [manual](https://manualoh.schockaert.tk/) where you can find lot's of information to set up openHAB with Qbus client and server (for the moment only in Dutch). +We also host a site which contains a [manual](https://manualoh.schockaert.tk/) where you can find lots of information to set up openHAB with Qbus client and server (for the moment only in Dutch). The controllers can not communicate directly with openHAB, therefore we developed a client/server application which you must install prior to enable this binding. More information can be found here: -[Qbus Client/Server]( https://github.com/QbusKoen/QbusClientServer-Installer) +[Qbus Client/Server](https://github.com/QbusKoen/QbusClientServer-Installer) With this binding you can control and read almost every output from the Qbus system. @@ -41,43 +41,38 @@ For now the following Qbus things are not yet supported but will come: The discovery service is not yet implemented but the System Manager III software of Qbus generates things and item files from the programming, which you can use directly in openHAB. -## Thing Configuration - -This is an example of the things configuration file: +## Bridge configuration ``` Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, refresh=10 ] { - dimmer 1 "ToonzaalLED" [ dimmerId=100 ] - onOff 30 "Toonzaal230V" [ bistabielId=76 ] - thermostat 50 "Service" [ thermostatId=99 ] - scene 70 "Disco" [ sceneId=36 ] - co2 100 "Productie" [ co2Id=26 ] - rollershutter 120 "Roller1" [ rolId=268 ] - rollershutter_slats 121 "Roller2" [ rolId=264 ] +... } ``` -| Property | Default | Required | Description | -|------------|----------|----------|----------------| -| `addr` | localhost | YES | The ip address of the machine where the Qbus Server runs | -| `sn` | | YES | The serial number of your controller | -| `port` | 8447 | YES | The communication port of the client/server | + +| Property | Default | Required | Description | +| --------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `addr` | localhost | YES | The ip address of the machine where the Qbus Server runs | +| `sn` | | YES | The serial number of your controller | +| `port` | 8447 | YES | The communication port of the client/server | | `refresh` | 5 | NO | Refresh time - After x minutes there will be a check if server is still running and if client is still connected. If not - reconnect | -## Channels - -| Thing Type ID | Channel Name | Read only | description | -|---------------------|---------------|---------------------------------------------------------| -| `onOff` | switch | No | This is the channel for Bistable, Timers and Intervals | -| `dimmer` | brightness | No | This is the channel for Dimmers 1&2 buttons and CLC | -| `scene` | Switch | No | This is the channel for scenes | -| `co2` | co2 | Yes | This is the channel for CO2 sensors | -| `rollershutter` | rollershutter | No | This is the channel for rollershutters | -| `rollershutter_slats` | rollershutter | No | This is the channel for rollershutters with slats | -| `thermostat` | setpoint | No | This is the channel for thermostats setpoint | -| `thermostat` | measured | Yes | This is the channel for thermostats currenttemp | -| `thermostat` | mode | No | This is the channel for thermostats mode | + + +## Things configuration + +| Thing Type ID | Channel Name | Read only | description | +| --------------------- | ------------- | --------- | ------------------------------------------------------ | +| `onOff` | switch | No | This is the channel for Bistable, Timers and Intervals | +| `dimmer` | brightness | No | This is the channel for Dimmers 1&2 buttons and CLC | +| `scene` | Switch | No | This is the channel for scenes | +| `co2` | co2 | Yes | This is the channel for CO2 sensors | +| `rollershutter` | rollershutter | No | This is the channel for rollershutters | +| `rollershutter_slats` | rollershutter | No | This is the channel for rollershutters with slats | +| `thermostat` | setpoint | No | This is the channel for thermostats setpoint | +| `thermostat` | measured | Yes | This is the channel for thermostats currenttemp | +| `thermostat` | mode | No | This is the channel for thermostats mode | ## Full Example From 3551d12972df7f17ed5deafe023227f8175477b7 Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Sat, 9 Jan 2021 14:02:06 +0100 Subject: [PATCH 07/17] Updated requested changes Signed-off-by: QbusKoen --- .../qbus/internal/QbusBridgeHandler.java | 54 ++++++------------- .../internal/protocol/QbusCommunication.java | 3 +- 2 files changed, 17 insertions(+), 40 deletions(-) diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java index 44c3d2a8d8dd6..580f3a32f02ba 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java @@ -15,15 +15,12 @@ import java.net.InetAddress; import java.net.UnknownHostException; -import java.util.Map; -import java.util.Map.Entry; 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.qbus.internal.protocol.QbusCommunication; -import org.openhab.core.config.core.Configuration; import org.openhab.core.thing.Bridge; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ThingStatus; @@ -59,12 +56,17 @@ public QbusBridgeHandler(Bridge Bridge) { */ @Override public void initialize() { - logger.info("Initializing bridge handler"); - - setConfig(); - InetAddress addr = getAddr(); + readConfig(); + InetAddress addr = null; int port = getPort(); + try { + addr = InetAddress.getByName(getAddress()); + } catch (UnknownHostException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "No or incorrect ip address set for Qbus Server"); + } + if (addr != null) { createCommunicationObject(addr, port); } else { @@ -156,7 +158,6 @@ private void setupRefreshTimer(int refreshInterval) { } // This timer will check connection with server and client periodically - logger.info("Check communication with Server and Client every {} min", refreshInterval); refreshTimer = scheduler.scheduleWithFixedDelay(() -> { logger.info("Checking connection with Qbus Server & Client."); @@ -165,18 +166,14 @@ private void setupRefreshTimer(int refreshInterval) { if (comm != null) { if (!comm.communicationActive()) { // Disconnected from Qbus Server, try to reconnect - logger.info("No connection with Qbus Server. Try to restart."); comm.restartCommunication(); if (!comm.communicationActive()) { bridgeOffline("No connection with Qbus Server"); return; } - } else { - // Controller disconnected from Qbus client, try to reconnect controller if (!comm.clientConnected()) { - logger.info("No connection with Qbus Client. Try to restart."); comm.restartCommunication(); if (!comm.clientConnected()) { bridgeOffline("No connection with Qbus Client"); @@ -213,22 +210,9 @@ public void dispose() { } /** - * Update the configuration parameters + * Sets the configuration parameters */ - @Override - public void handleConfigurationUpdate(Map configurationParameters) { - Configuration configuration = editConfiguration(); - for (Entry configurationParmeter : configurationParameters.entrySet()) { - configuration.put(configurationParmeter.getKey(), configurationParmeter.getValue()); - } - updateConfiguration(configuration); - updateStatus(ThingStatus.ONLINE); - } - - /** - * Sets the configuration prameters - */ - protected synchronized void setConfig() { + protected void readConfig() { config = getConfig().as(QbusConfiguration.class); } @@ -242,20 +226,13 @@ protected synchronized void setConfig() { } /** - * Get the IP-address of the Qbus server. + * Get the ip address of the Qbus server. * - * @return the addr + * @return the ip address */ - @SuppressWarnings("null") - public @Nullable InetAddress getAddr() { - InetAddress addr = null; - try { - addr = InetAddress.getByName(config.addr); - } catch (UnknownHostException e) { - logger.debug("Cannot resolve hostname {} to IP adress", config.addr); - } - return addr; + public String getAddress() { + return config.addr; } /** @@ -290,6 +267,5 @@ public int getRefresh() { @Override public void handleCommand(ChannelUID channelUID, Command command) { - // TODO Auto-generated method stub } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java index 347c50a32662e..b5ca4ba566148 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java @@ -119,7 +119,8 @@ public synchronized void startCommunication() { throw new IOException(); } - InetAddress addr = handler.getAddr(); + InetAddress addr = null; + addr = InetAddress.getByName(handler.getAddress()); int port = handler.getPort(); Socket socket = new Socket(addr, port); From ec2e2ac2cd4bb64719e74315439d37bfd7b4f524 Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Sat, 9 Jan 2021 14:12:35 +0100 Subject: [PATCH 08/17] Updated headers Signed-off-by: QbusKoen --- .../qbus/internal/QbusBindingConstants.java | 2 +- .../qbus/internal/QbusBridgeHandler.java | 2 +- .../qbus/internal/QbusConfiguration.java | 2 +- .../qbus/internal/QbusHandlerFactory.java | 2 +- .../handler/QbusBistabielHandler.java | 2 +- .../qbus/internal/handler/QbusCO2Handler.java | 2 +- .../internal/handler/QbusDimmerHandler.java | 2 +- .../internal/handler/QbusGlobalHandler.java | 2 +- .../qbus/internal/handler/QbusRolHandler.java | 2 +- .../internal/handler/QbusSceneHandler.java | 2 +- .../handler/QbusThermostatHandler.java | 2 +- .../internal/handler/QbusThingsConfig.java | 2 +- .../qbus/internal/protocol/QbusBistabiel.java | 2 +- .../qbus/internal/protocol/QbusCO2.java | 2 +- .../internal/protocol/QbusCommunication.java | 24 +------------------ .../qbus/internal/protocol/QbusDimmer.java | 2 +- .../internal/protocol/QbusMessageBase.java | 2 +- .../internal/protocol/QbusMessageCmd.java | 2 +- .../protocol/QbusMessageDeserializer.java | 2 +- .../internal/protocol/QbusMessageListMap.java | 2 +- .../internal/protocol/QbusMessageMap.java | 2 +- .../qbus/internal/protocol/QbusRol.java | 2 +- .../qbus/internal/protocol/QbusScene.java | 2 +- .../internal/protocol/QbusThermostat.java | 2 +- 24 files changed, 24 insertions(+), 46 deletions(-) diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java index 90df9ac47a12e..c04f9899511a9 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java index 580f3a32f02ba..33b1c7b98f721 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java index cd05e2bd585df..0bd53e225088b 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java index 33d29d2d03253..9a2b13cb1cc33 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java index 8c406b86378f1..e305b810af604 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java index 4bffbc9104885..1b4fd8925b75c 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java index 5a6ad5d981bad..f909ae019cf5c 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java index 48a05c95341ec..b2bdf0594c5ec 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java index bf8fcb67d26df..afacd7bf62aa8 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java index ba00300fc1001..d70bf35bf2b85 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java index c03dbcf282c0d..12ae5771d11ae 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java index 24b4b2160b77f..49cda1d052c47 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java index bf0fca3867252..6ea8b74a1185e 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java index e8e8d1ee5c696..f83dc99efd872 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java index b5ca4ba566148..fa222a68b1b7d 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. @@ -102,12 +102,10 @@ public QbusCommunication() { public synchronized void startCommunication() { QbusBridgeHandler handler = bridgeCallBack; - try { for (int i = 1; qEventsRunning && (i <= 5); i++) { Thread.sleep(1000); - } if (qEventsRunning) { logger.error("Starting from thread {}, but previous connection still active after 5000ms", @@ -264,7 +262,6 @@ public boolean clientConnected() { synchronized void sendMessage(Object qMessage) { PrintWriter writer = qOut; String json = gsonOut.toJson(qMessage); - logger.debug("Send json from thread {}", Thread.currentThread().getId()); if (writer != null) { writer.println(json); @@ -280,7 +277,6 @@ synchronized void sendMessage(Object qMessage) { logger.warn("Error sending message, trying to restart communication"); restartCommunication(); // retry sending after restart - logger.debug("Resend json from thread {}", Thread.currentThread().getId()); writer = qOut; if (writer != null) { writer.println(json); @@ -312,9 +308,7 @@ private void sendAndReadMessage(String command) throws IOException, InterruptedE * * @param qMessage message read from Qbus. */ - // @SuppressWarnings("null") private void readMessage(String qMessage) { - logger.debug("Received json on thread {}", Thread.currentThread().getId()); String confsn = ""; String cmd = ""; String CTD = ""; @@ -414,7 +408,6 @@ private void initialize() throws IOException, InterruptedException { logger.info("Requesting CO2 outputs from client"); sendAndReadMessage("getCo2"); } else { - logger.warn("No CTD client connected to server with sn {}", CTD); CTDConnected = false; QbusBridgeHandler handler = bridgeCallBack; @@ -440,7 +433,6 @@ private void Connect() throws InterruptedException, IOException { if (bridgeCallBack != null) { CTD = bridgeCallBack.getSn(); } - logger.info("Connecting to server"); QbusMessageCmd QCmd = new QbusMessageCmd(CTD, "openHAB"); sendMessage(QCmd); @@ -459,8 +451,6 @@ private void Connect() throws InterruptedException, IOException { */ @SuppressWarnings("null") private void cmdListBistabiel(@Nullable List> outputs) { - logger.info("Bistabiel/Timers/Monos/Intervals received from Qbus server"); - if (outputs != null) { for (Map bistabiel : outputs) { String idStr = bistabiel.get("id"); @@ -491,8 +481,6 @@ private void cmdListBistabiel(@Nullable List> outputs) { * @param outputs */ private void cmdlistscenes(@Nullable List> outputs) { - logger.info("Scenes received from Qbus server"); - if (outputs != null) { for (Map scene : outputs) { String idStr = scene.get("id"); @@ -515,8 +503,6 @@ private void cmdlistscenes(@Nullable List> outputs) { */ @SuppressWarnings("null") private void cmdListDimmers(@Nullable List> outputs) { - logger.info("Dimmers received from the Qbus server"); - if (outputs != null) { for (Map dimmer : outputs) { String idStr = dimmer.get("id"); @@ -546,8 +532,6 @@ private void cmdListDimmers(@Nullable List> outputs) { */ @SuppressWarnings("null") private void cmdlistrol(@Nullable List> outputs) { - - logger.info("ROL02P received from Qbus server"); if (outputs != null) { for (Map rol : outputs) { String idStr = rol.get("id"); @@ -577,8 +561,6 @@ private void cmdlistrol(@Nullable List> outputs) { */ @SuppressWarnings("null") private void cmdlistrolslats(@Nullable List> outputs) { - logger.info("ROL02PSLATS received from Qbus server"); - if (outputs != null) { for (Map rol : outputs) { @@ -615,8 +597,6 @@ private void cmdlistrolslats(@Nullable List> outputs) { */ @SuppressWarnings("null") private void cmdlistco2(@Nullable List> outputs) { - logger.info("CO2 received from Qbus server"); - if (outputs != null) { for (Map co2 : outputs) { String idStr = co2.get("id"); @@ -649,8 +629,6 @@ private void cmdlistco2(@Nullable List> outputs) { */ @SuppressWarnings("null") private void cmdListThermostat(@Nullable List> outputs) { - logger.info("Thermostats received from the Qbus server"); - if (outputs != null) { for (Map thermostat : outputs) { String idStr = thermostat.get("id"); diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java index d2cdb3c674090..39589001b9444 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java index 5bc33a2c4aed5..d14cc63ba1ba7 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java index 34a4140c257c3..9ee5eb63c657c 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java index 5fb2b9a88cbe8..17a9edc669126 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageListMap.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageListMap.java index 66a30947eb08e..01fefbd74ba87 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageListMap.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageListMap.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageMap.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageMap.java index f061119b1ebb9..097d0841429ca 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageMap.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageMap.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java index 9933f643996e0..9d63d9ecf7f74 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java index e1cfa4f0a0697..adca51362c772 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java index 68da81b61352e..294afba6f42bb 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2010-2020 Contributors to the openHAB project + * Copyright (c) 2010-2021 Contributors to the openHAB project * * See the NOTICE file(s) distributed with this work for additional * information. From d681e87c89ebf6d681fc68fe5ed6a10f8ba22735 Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Tue, 9 Feb 2021 10:40:16 +0100 Subject: [PATCH 09/17] Code fixing Update code like requested by fwolter on 3/1/2021 Signed-off-by: QbusKoen --- .../qbus/internal/QbusBindingConstants.java | 1 + .../qbus/internal/QbusBridgeHandler.java | 77 ++-- .../qbus/internal/QbusConfiguration.java | 9 +- .../handler/QbusBistabielHandler.java | 139 ++++-- .../qbus/internal/handler/QbusCO2Handler.java | 112 +++-- .../internal/handler/QbusDimmerHandler.java | 236 ++++++---- .../internal/handler/QbusGlobalHandler.java | 4 +- .../qbus/internal/handler/QbusRolHandler.java | 288 +++++++++---- .../internal/handler/QbusSceneHandler.java | 168 +++++--- .../handler/QbusThermostatHandler.java | 165 ++++--- .../qbus/internal/protocol/QbusBistabiel.java | 23 +- .../qbus/internal/protocol/QbusCO2.java | 23 +- .../internal/protocol/QbusCommunication.java | 408 +++++++++--------- .../qbus/internal/protocol/QbusDimmer.java | 23 +- .../internal/protocol/QbusMessageBase.java | 31 +- .../qbus/internal/protocol/QbusRol.java | 47 +- .../qbus/internal/protocol/QbusScene.java | 66 +-- .../internal/protocol/QbusThermostat.java | 11 - .../resources/OH-INF/thing/thing-types.xml | 19 + 19 files changed, 1124 insertions(+), 726 deletions(-) diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java index c04f9899511a9..cf2f9d983f27e 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java @@ -63,6 +63,7 @@ public class QbusBindingConstants { // List of all Channel ids public static final String CHANNEL_SWITCH = "switch"; + public static final String CHANNEL_SCENE = "scene"; public static final String CHANNEL_BRIGHTNESS = "brightness"; public static final String CHANNEL_MEASURED = "measured"; public static final String CHANNEL_SETPOINT = "setpoint"; diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java index 33b1c7b98f721..2de20eb45c95c 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java @@ -27,8 +27,6 @@ import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseBridgeHandler; import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * {@link QbusBridgeHandler} is the handler for a Qbus controller @@ -39,11 +37,9 @@ @NonNullByDefault public class QbusBridgeHandler extends BaseBridgeHandler { - private final Logger logger = LoggerFactory.getLogger(QbusBridgeHandler.class); - private @Nullable QbusCommunication qbusComm; - protected @Nullable QbusConfiguration config; + protected @NonNullByDefault({}) QbusConfiguration config; private @Nullable ScheduledFuture refreshTimer; @@ -57,24 +53,33 @@ public QbusBridgeHandler(Bridge Bridge) { @Override public void initialize() { readConfig(); - InetAddress addr = null; - int port = getPort(); + InetAddress addr; + Integer port = getPort(); + Integer Refresh = getRefresh(); + + if (port == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "No port defined for Qbus Server"); + return; + } try { addr = InetAddress.getByName(getAddress()); + if (addr == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "No ip address defined for Qbus Server"); + return; + } else { + createCommunicationObject(addr, port); + } } catch (UnknownHostException e) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "No or incorrect ip address set for Qbus Server"); + "Incorrect ip address set for Qbus Server"); } - if (addr != null) { - createCommunicationObject(addr, port); - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Cannot connect to QbusServer with hostname " + addr); + if (Refresh != null) { + this.setupRefreshTimer(Refresh); } - int refreshInterval = getRefresh(); - this.setupRefreshTimer(refreshInterval); } /** @@ -159,8 +164,6 @@ private void setupRefreshTimer(int refreshInterval) { // This timer will check connection with server and client periodically refreshTimer = scheduler.scheduleWithFixedDelay(() -> { - logger.info("Checking connection with Qbus Server & Client."); - QbusCommunication comm = getCommunication(); if (comm != null) { @@ -182,8 +185,6 @@ private void setupRefreshTimer(int refreshInterval) { } } } - - logger.info("Connection with Qbus Server & Client is still active."); updateStatus(ThingStatus.ONLINE); }, refreshInterval, refreshInterval, TimeUnit.MINUTES); @@ -230,19 +231,25 @@ protected void readConfig() { * * @return the ip address */ - @SuppressWarnings("null") - public String getAddress() { - return config.addr; + public @Nullable String getAddress() { + if (config != null) { + return config.addr; + } else { + return null; + } } /** * Get the listening port of the Qbus server. * - * @return the port + * @return */ - @SuppressWarnings("null") - public int getPort() { - return config.port; + public @Nullable Integer getPort() { + if (config != null) { + return config.port; + } else { + return null; + } } /** @@ -250,9 +257,12 @@ public int getPort() { * * @return the serial nr of the controller */ - @SuppressWarnings("null") - public String getSn() { - return config.sn; + public @Nullable String getSn() { + if (config != null) { + return config.sn; + } else { + return null; + } } /** @@ -260,9 +270,12 @@ public String getSn() { * * @return the refresh interval */ - @SuppressWarnings("null") - public int getRefresh() { - return config.refresh; + public @Nullable Integer getRefresh() { + if (config != null) { + return config.refresh; + } else { + return null; + } } @Override diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java index 0bd53e225088b..533822033e644 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java @@ -14,6 +14,7 @@ package org.openhab.binding.qbus.internal; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; /** * Class {@link QbusConfiguration} Configuration Class @@ -23,8 +24,8 @@ @NonNullByDefault public class QbusConfiguration { - public String addr = ""; - public int port; - public String sn = ""; - public int refresh; + public @Nullable String addr; + public @Nullable Integer port; + public @Nullable String sn; + public @Nullable Integer refresh; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java index e305b810af604..cd72eb6f75e30 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java @@ -15,6 +15,8 @@ import static org.openhab.binding.qbus.internal.QbusBindingConstants.CHANNEL_SWITCH; import static org.openhab.core.types.RefreshType.REFRESH; +import java.util.Map; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.QbusBridgeHandler; @@ -26,8 +28,6 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusBistabielHandler} is responsible for handling the Bistable outputs of Qbus @@ -38,17 +38,16 @@ @NonNullByDefault public class QbusBistabielHandler extends QbusGlobalHandler { - private final Logger logger = LoggerFactory.getLogger(QbusBistabielHandler.class); - public QbusBistabielHandler(Thing thing) { super(thing); } - protected @Nullable QbusThingsConfig config; + protected @NonNullByDefault({}) QbusThingsConfig config; - int bistabielId = 0; + int bistabielId; - String sn = ""; + @Nullable + private String sn; /** * Main initialization @@ -56,7 +55,7 @@ public QbusBistabielHandler(Thing thing) { @Override public void initialize() { - setConfig(); + readConfig(); bistabielId = getId(); QbusCommunication QComm = getCommunication("Bistabiel", bistabielId); @@ -68,20 +67,50 @@ public void initialize() { QbusBridgeHandler QBridgeHandler = getBridgeHandler("Bistabiel", bistabielId); if (QBridgeHandler == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "No communication with Qbus Bridge!"); return; } - QbusBistabiel QBistabiel = QComm.getBistabiel().get(bistabielId); + setSN(); - sn = QBridgeHandler.getSn(); + Map bistabielComm = QComm.getBistabiel(); - if (QBistabiel != null) { - QBistabiel.setThingHandler(this); - handleStateUpdate(QBistabiel); - logger.info("Bistabiel intialized {}", bistabielId); + if (bistabielComm != null) { + QbusBistabiel QBistabiel = bistabielComm.get(bistabielId); + if (QBistabiel != null) { + QBistabiel.setThingHandler(this); + handleStateUpdate(QBistabiel); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Error while initializing the thing."); + } } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Error while initializing the thing."); + } + } - logger.warn("Bistabiel not intialized {}", bistabielId); + /** + * Returns the serial number of the controller + * + * @return the serial nr + */ + public @Nullable String getSN() { + return this.sn; + } + + /** + * Sets the serial number of the controller + */ + public void setSN() { + QbusBridgeHandler QBridgeHandler = getBridgeHandler("Bistabiel", bistabielId); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "No communication with Qbus Bridge!"); + return; + } else { + this.sn = QBridgeHandler.getSn(); } } @@ -96,30 +125,39 @@ public void handleCommand(ChannelUID channelUID, Command command) { "Bridge communication not initialized when trying to execute command for bistabiel " + bistabielId); return; } - QbusBistabiel QBistabiel = QComm.getBistabiel().get(bistabielId); - if (QBistabiel == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for dimmer " + bistabielId); - return; - } + Map bistabielComm = QComm.getBistabiel(); - scheduler.submit(() -> { - if (!QComm.communicationActive()) { - restartCommunication(QComm, "Bistabiel", bistabielId); - } + if (bistabielComm != null) { + QbusBistabiel QBistabiel = bistabielComm.get(bistabielId); - if (QComm.communicationActive()) { + if (QBistabiel == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for bistabiel " + + bistabielId); + return; + } else { + scheduler.submit(() -> { + if (!QComm.communicationActive()) { + restartCommunication(QComm, "Bistabiel", bistabielId); + } - if (command == REFRESH) { - handleStateUpdate(QBistabiel); - return; - } + if (QComm.communicationActive()) { - handleSwitchCommand(QBistabiel, channelUID, command); - } + if (command == REFRESH) { + handleStateUpdate(QBistabiel); + return; + } - }); + handleSwitchCommand(QBistabiel, channelUID, command); + } + + }); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Error while initializing the thing."); + } } /** @@ -128,11 +166,17 @@ public void handleCommand(ChannelUID channelUID, Command command) { private void handleSwitchCommand(QbusBistabiel QBistabiel, ChannelUID channelUID, Command command) { if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; - - if (s == OnOffType.OFF) { - QBistabiel.execute(0, sn); + @Nullable + String snr = getSN(); + if (snr != null) { + if (s == OnOffType.OFF) { + QBistabiel.execute(0, snr); + } else { + QBistabiel.execute(100, snr); + } } else { - QBistabiel.execute(100, sn); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "No serial number configured for " + bistabielId); } } } @@ -141,27 +185,30 @@ private void handleSwitchCommand(QbusBistabiel QBistabiel, ChannelUID channelUID * Method to update state of channel, called from Qbus Bistabiel. */ public void handleStateUpdate(QbusBistabiel QBistabiel) { - - int bistabielState = QBistabiel.getState(); - - updateState(CHANNEL_SWITCH, (bistabielState == 0) ? OnOffType.OFF : OnOffType.ON); - updateStatus(ThingStatus.ONLINE); + Integer bistabielState = QBistabiel.getState(); + if (bistabielState != null) { + updateState(CHANNEL_SWITCH, (bistabielState == 0) ? OnOffType.OFF : OnOffType.ON); + updateStatus(ThingStatus.ONLINE); + } } /** * Read the configuration */ - protected synchronized void setConfig() { + protected synchronized void readConfig() { config = getConfig().as(QbusThingsConfig.class); } /** * Returns the Id from the configuration * - * @return bistabielId + * @return */ - @SuppressWarnings("null") public int getId() { - return config.bistabielId; + if (config != null) { + return config.bistabielId; + } else { + return 0; + } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java index 1b4fd8925b75c..500953834205e 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java @@ -15,7 +15,10 @@ import static org.openhab.binding.qbus.internal.QbusBindingConstants.CHANNEL_CO2; import static org.openhab.core.types.RefreshType.REFRESH; +import java.util.Map; + import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.QbusBridgeHandler; import org.openhab.binding.qbus.internal.protocol.QbusCO2; import org.openhab.binding.qbus.internal.protocol.QbusCommunication; @@ -25,8 +28,6 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusCO2Handler} is responsible for handling commands, which are @@ -38,17 +39,16 @@ @NonNullByDefault public class QbusCO2Handler extends QbusGlobalHandler { - private final Logger logger = LoggerFactory.getLogger(QbusCO2Handler.class); - public QbusCO2Handler(Thing thing) { super(thing); } - protected QbusThingsConfig config = getConfig().as(QbusThingsConfig.class);; + protected @NonNullByDefault({}) QbusThingsConfig config; int co2Id = 0; - String sn = ""; + @Nullable + String sn; /** * Main initialization @@ -71,16 +71,22 @@ public void initialize() { return; } - QbusCO2 QCo2 = QComm.getCo2().get(co2Id); + setSN(); - sn = QBridgeHandler.getSn(); + Map co2Comm = QComm.getCo2(); - if (QCo2 != null) { - QCo2.setThingHandler(this); - handleStateUpdate(QCo2); - logger.info("CO2 intialized {}", co2Id); + if (co2Comm != null) { + QbusCO2 QCo2 = co2Comm.get(co2Id); + if (QCo2 != null) { + QCo2.setThingHandler(this); + handleStateUpdate(QCo2); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Error while initializing the thing."); + } } else { - logger.info("CO2 not intialized {}", co2Id); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Error while initializing the thing."); } } @@ -96,38 +102,65 @@ public void handleCommand(ChannelUID channelUID, Command command) { return; } - QbusCO2 QCo2 = QComm.getCo2().get(co2Id); - - if (QCo2 == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for CO2 " + co2Id); - return; - } - - scheduler.submit(() -> { - if (!QComm.communicationActive()) { - restartCommunication(QComm, "CO2", co2Id); - } - - if (QComm.communicationActive()) { - if (command == REFRESH) { - handleStateUpdate(QCo2); - return; - } - + Map co2Comm = QComm.getCo2(); + + if (co2Comm != null) { + QbusCO2 QCo2 = co2Comm.get(co2Id); + if (QCo2 == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for CO2 " + co2Id); + return; + } else { + + scheduler.submit(() -> { + if (!QComm.communicationActive()) { + restartCommunication(QComm, "CO2", co2Id); + } + + if (QComm.communicationActive()) { + if (command == REFRESH) { + handleStateUpdate(QCo2); + return; + } + + } + }); } - }); + } } /** * Method to update state of channel, called from Qbus CO2. */ public void handleStateUpdate(QbusCO2 QCo2) { + Integer CO2State = QCo2.getState(); + if (CO2State != null) { + updateState(CHANNEL_CO2, new DecimalType(CO2State)); + updateStatus(ThingStatus.ONLINE); + } + } - int CO2State = QCo2.getState(); + /** + * Returns the serial number of the controller + * + * @return the serial nr + */ + public @Nullable String getSN() { + return this.sn; + } - updateState(CHANNEL_CO2, new DecimalType(CO2State)); - updateStatus(ThingStatus.ONLINE); + /** + * Sets the serial number of the controller + */ + public void setSN() { + QbusBridgeHandler QBridgeHandler = getBridgeHandler("CO2", co2Id); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "No communication with Qbus Bridge!"); + return; + } else { + this.sn = QBridgeHandler.getSn(); + } } /** @@ -142,8 +175,11 @@ protected synchronized void setConfig() { * * @return co2Id */ - public int getId() { - return config.co2Id; + if (config != null) { + return config.co2Id; + } else { + return 0; + } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java index f909ae019cf5c..a5d2193320bcc 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java @@ -15,6 +15,8 @@ import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; import static org.openhab.core.types.RefreshType.REFRESH; +import java.util.Map; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.QbusBridgeHandler; @@ -28,8 +30,6 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusDimmerHandler} is responsible for handling the dimmable outputs of Qbus @@ -40,20 +40,19 @@ @NonNullByDefault public class QbusDimmerHandler extends QbusGlobalHandler { - private final Logger logger = LoggerFactory.getLogger(QbusDimmerHandler.class); - public QbusDimmerHandler(Thing thing) { super(thing); } - protected @Nullable QbusThingsConfig config; + protected @NonNullByDefault({}) QbusThingsConfig config; - int dimmerId = 0; + int dimmerId; - String sn = ""; + @Nullable + private String sn; /** - * Main initialisation + * Main initialization */ @Override public void initialize() { @@ -72,19 +71,47 @@ public void initialize() { return; } - QbusDimmer QDimmer = QComm.getDimmer().get(dimmerId); + setSN(); - sn = QBridgeHandler.getSn(); - - if (QDimmer != null) { - QDimmer.setThingHandler(this); - handleStateUpdate(QDimmer); - logger.info("Dimmer intialized {}", dimmerId); + Map dimmerComm = QComm.getDimmer(); + if (dimmerComm != null) { + QbusDimmer QDimmer = dimmerComm.get(dimmerId); + if (QDimmer != null) { + QDimmer.setThingHandler(this); + handleStateUpdate(QDimmer); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Error while initializing the thing."); + } } else { - logger.warn("Dimmer not intialized {}", dimmerId); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Error while initializing the thing."); } } + /** + * Returns the serial number of the controller + * + * @return the serial nr + */ + public @Nullable String getSN() { + return this.sn; + } + + /** + * Sets the serial number of the controller + */ + public void setSN() { + QbusBridgeHandler QBridgeHandler = getBridgeHandler("Dimmer", dimmerId); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "No communication with Qbus Bridge!"); + return; + } + this.sn = QBridgeHandler.getSn(); + ; + } + /** * Handle the status update from the thing */ @@ -98,43 +125,58 @@ public void handleCommand(ChannelUID channelUID, Command command) { return; } - QbusDimmer QDimmer = QComm.getDimmer().get(dimmerId); + Map dimmerComm = QComm.getDimmer(); + if (dimmerComm != null) { + QbusDimmer QDimmer = dimmerComm.get(dimmerId); - if (QDimmer == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for dimmer " + dimmerId); - return; - } + if (QDimmer == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for dimmer " + dimmerId); + return; + } else { - scheduler.submit(() -> { - if (!QComm.communicationActive()) { - restartCommunication(QComm, "Dimmer", dimmerId); + scheduler.submit(() -> { + if (!QComm.communicationActive()) { + restartCommunication(QComm, "Dimmer", dimmerId); + } + + if (QComm.communicationActive()) { + + if (command == REFRESH) { + handleStateUpdate(QDimmer); + return; + } + + switch (channelUID.getId()) { + case CHANNEL_SWITCH: + handleSwitchCommand(QDimmer, command); + updateStatus(ThingStatus.ONLINE); + break; + + case CHANNEL_BRIGHTNESS: + handleBrightnessCommand(QDimmer, command); + updateStatus(ThingStatus.ONLINE); + break; + + default: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Channel unknown " + channelUID.getId()); + } + } + }); } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Error while initializing the thing."); + } + } - if (QComm.communicationActive()) { - - if (command == REFRESH) { - handleStateUpdate(QDimmer); - return; - } - - switch (channelUID.getId()) { - case CHANNEL_SWITCH: - handleSwitchCommand(QDimmer, command); - updateStatus(ThingStatus.ONLINE); - break; - - case CHANNEL_BRIGHTNESS: - handleBrightnessCommand(QDimmer, command); - updateStatus(ThingStatus.ONLINE); - break; - - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Channel unknown " + channelUID.getId()); - } - } - }); + /** + * + * @param message + */ + public void thingOffline(String message) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); } /** @@ -143,10 +185,21 @@ public void handleCommand(ChannelUID channelUID, Command command) { private void handleSwitchCommand(QbusDimmer QDimmer, Command command) { if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; + @Nullable + String snr = getSN(); + if (s == OnOffType.OFF) { - QDimmer.execute(0, sn); + if (snr != null) { + QDimmer.execute(0, snr); + } else { + thingOffline("No serial number configured for " + dimmerId); + } } else { - QDimmer.execute(100, sn); + if (snr != null) { + QDimmer.execute(1000, snr); + } else { + thingOffline("No serial number configured for " + dimmerId); + } } } } @@ -155,35 +208,67 @@ private void handleSwitchCommand(QbusDimmer QDimmer, Command command) { * Executes the brightness command */ private void handleBrightnessCommand(QbusDimmer QDimmer, Command command) { + @Nullable + String snr = getSN(); if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; if (s == OnOffType.OFF) { - QDimmer.execute(0, sn); + if (snr != null) { + QDimmer.execute(0, snr); + } else { + thingOffline("No serial number configured for " + dimmerId); + } } else { - QDimmer.execute(100, sn); + if (snr != null) { + QDimmer.execute(100, snr); + } else { + thingOffline("No serial number configured for " + dimmerId); + } } } else if (command instanceof IncreaseDecreaseType) { IncreaseDecreaseType s = (IncreaseDecreaseType) command; int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); - int currentValue = QDimmer.getState(); - int newValue; - if (s == IncreaseDecreaseType.INCREASE) { - newValue = currentValue + stepValue; - // round down to step multiple - newValue = newValue - newValue % stepValue; - QDimmer.execute(newValue > 100 ? 100 : newValue, sn); - } else { - newValue = currentValue - stepValue; - // round up to step multiple - newValue = newValue + newValue % stepValue; - QDimmer.execute(newValue < 0 ? 0 : newValue, sn); + Integer currentValue = QDimmer.getState(); + Integer newValue; + Integer sendvalue; + if (currentValue != null) { + if (s == IncreaseDecreaseType.INCREASE) { + newValue = currentValue + stepValue; + // round down to step multiple + newValue = newValue - newValue % stepValue; + sendvalue = newValue > 100 ? 100 : newValue; + if (snr != null) { + QDimmer.execute(sendvalue, snr); + } else { + thingOffline("No serial number configured for " + dimmerId); + } + } else { + newValue = currentValue - stepValue; + // round up to step multiple + newValue = newValue + newValue % stepValue; + sendvalue = newValue < 0 ? 0 : newValue; + if (snr != null) { + QDimmer.execute(sendvalue, snr); + } else { + thingOffline("No serial number configured for " + dimmerId); + } + } } } else if (command instanceof PercentType) { PercentType p = (PercentType) command; + int pp = p.intValue(); if (p == PercentType.ZERO) { - QDimmer.execute(0, sn); + if (snr != null) { + QDimmer.execute(0, snr); + } else { + thingOffline("No serial number configured for " + dimmerId); + } } else { - QDimmer.execute(p.intValue(), sn); + if (snr != null) { + QDimmer.execute(pp, snr); + } else { + thingOffline("No serial number configured for " + dimmerId); + } } } } @@ -193,11 +278,11 @@ private void handleBrightnessCommand(QbusDimmer QDimmer, Command command) { */ public void handleStateUpdate(QbusDimmer QDimmer) { - int dimmerState = QDimmer.getState(); - - updateState(CHANNEL_BRIGHTNESS, new PercentType(dimmerState)); - - updateStatus(ThingStatus.ONLINE); + Integer dimmerState = QDimmer.getState(); + if (dimmerState != null) { + updateState(CHANNEL_BRIGHTNESS, new PercentType(dimmerState)); + updateStatus(ThingStatus.ONLINE); + } } /** @@ -212,8 +297,11 @@ protected synchronized void setConfig() { * * @return dimmerId */ - @SuppressWarnings("null") public int getId() { - return config.dimmerId; + if (config != null) { + return config.dimmerId; + } else { + return 0; + } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java index b2bdf0594c5ec..f25448570ade2 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java @@ -45,7 +45,7 @@ public QbusGlobalHandler(Thing thing) { public @Nullable QbusCommunication getCommunication(String type, int globalId) { QbusBridgeHandler QBridgeHandler = getBridgeHandler(type, globalId); if (QBridgeHandler == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED, "No bridge handler initialized for " + type + " with id " + globalId + "."); return null; } @@ -63,7 +63,7 @@ public QbusGlobalHandler(Thing thing) { public @Nullable QbusBridgeHandler getBridgeHandler(String type, int globalId) { Bridge QBridge = getBridge(); if (QBridge == null) { - updateStatus(ThingStatus.UNINITIALIZED, ThingStatusDetail.BRIDGE_UNINITIALIZED, + updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED, "No bridge initialized for " + type + " with ID " + globalId); return null; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java index afacd7bf62aa8..92baabd4368e7 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java @@ -15,6 +15,8 @@ import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; import static org.openhab.core.types.RefreshType.REFRESH; +import java.util.Map; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.QbusBridgeHandler; @@ -27,8 +29,6 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusRolHandler} is responsible for handling commands, which are @@ -40,17 +40,16 @@ @NonNullByDefault public class QbusRolHandler extends QbusGlobalHandler { - private final Logger logger = LoggerFactory.getLogger(QbusRolHandler.class); - public QbusRolHandler(Thing thing) { super(thing); } - protected @Nullable QbusThingsConfig config; + protected @NonNullByDefault({}) QbusThingsConfig config; - int rolId = 0; + int rolId; - String sn = ""; + @Nullable + String sn; /** * Main initialization @@ -73,19 +72,48 @@ public void initialize() { return; } - QbusRol QRol = QComm.getRol().get(rolId); + setSN(); - sn = QBridgeHandler.getSn(); + Map rolComm = QComm.getRol(); - if (QRol != null) { - QRol.setThingHandler(this); - handleStateUpdate(QRol); - logger.info("Screen/Store intialized {}", rolId); + if (rolComm != null) { + QbusRol QRol = rolComm.get(rolId); + if (QRol != null) { + QRol.setThingHandler(this); + handleStateUpdate(QRol); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Error while initializing the thing."); + } } else { - logger.warn("Screen/Store not intialized {}", rolId); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Error while initializing the thing."); } } + /** + * Returns the serial number of the controller + * + * @return the serial nr + */ + public @Nullable String getSN() { + return this.sn; + } + + /** + * Sets the serial number of the controller + */ + public void setSN() { + QbusBridgeHandler QBridgeHandler = getBridgeHandler("Screen/Store", rolId); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "No communication with Qbus Bridge!"); + return; + } + this.sn = QBridgeHandler.getSn(); + ; + } + /** * Handle the status update from the thing */ @@ -99,79 +127,123 @@ public void handleCommand(ChannelUID channelUID, Command command) { return; } - QbusRol QRol = QComm.getRol().get(rolId); - - if (QRol == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for ROL " + rolId); - return; - } + Map rolComm = QComm.getRol(); - scheduler.submit(() -> { - if (!QComm.communicationActive()) { - restartCommunication(QComm, "Screen/Store", rolId); + if (rolComm != null) { + QbusRol QRol = rolComm.get(rolId); + if (QRol == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for ROL " + rolId); + return; + } else { + scheduler.submit(() -> { + if (!QComm.communicationActive()) { + restartCommunication(QComm, "Screen/Store", rolId); + } + + if (QComm.communicationActive()) { + + if (command == REFRESH) { + handleStateUpdate(QRol); + return; + } + + switch (channelUID.getId()) { + case CHANNEL_ROLLERSHUTTER: + handleScreenposCommand(QRol, command); + updateStatus(ThingStatus.ONLINE); + break; + + case CHANNEL_SLATS: + handleSlatsposCommand(QRol, command); + updateStatus(ThingStatus.ONLINE); + break; + + default: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Channel unknown " + channelUID.getId()); + } + } + }); } + } + } - if (QComm.communicationActive()) { - - if (command == REFRESH) { - handleStateUpdate(QRol); - return; - } - - switch (channelUID.getId()) { - case CHANNEL_ROLLERSHUTTER: - handleScreenposCommand(QRol, command); - updateStatus(ThingStatus.ONLINE); - break; - - case CHANNEL_SLATS: - handleSlatsposCommand(QRol, command); - updateStatus(ThingStatus.ONLINE); - break; - - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Channel unknown " + channelUID.getId()); - } - } - }); + /** + * + * @param message + */ + public void thingOffline(String message) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); } /** * Executes the command for screen up/down position */ private void handleScreenposCommand(QbusRol QRol, Command command) { - + @Nullable + String snr = getSN(); if (command instanceof org.openhab.core.library.types.UpDownType) { org.openhab.core.library.types.UpDownType s = (org.openhab.core.library.types.UpDownType) command; if (s == org.openhab.core.library.types.UpDownType.DOWN) { - QRol.execute(0, sn); + if (snr != null) { + QRol.execute(0, snr); + } else { + thingOffline("No serial number configured for " + rolId); + } } else { - QRol.execute(100, sn); + if (snr != null) { + QRol.execute(100, snr); + } else { + thingOffline("No serial number configured for " + rolId); + } } - } else if (command instanceof IncreaseDecreaseType) { + } else if (command instanceof IncreaseDecreaseType) + + { IncreaseDecreaseType s = (IncreaseDecreaseType) command; int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); - int currentValue = QRol.getState(); + Integer currentValue = QRol.getState(); int newValue; - if (s == IncreaseDecreaseType.INCREASE) { - newValue = currentValue + stepValue; - // round down to step multiple - newValue = newValue - newValue % stepValue; - QRol.execute(newValue > 100 ? 100 : newValue, sn); - } else { - newValue = currentValue - stepValue; - // round up to step multiple - newValue = newValue + newValue % stepValue; - QRol.execute(newValue < 0 ? 0 : newValue, sn); + int sendValue; + if (currentValue != null) { + if (s == IncreaseDecreaseType.INCREASE) { + newValue = currentValue + stepValue; + // round down to step multiple + newValue = newValue - newValue % stepValue; + sendValue = newValue > 100 ? 100 : newValue; + if (snr != null) { + QRol.execute(sendValue, snr); + } else { + thingOffline("No serial number configured for " + rolId); + } + } else { + newValue = currentValue - stepValue; + // round up to step multiple + newValue = newValue + newValue % stepValue; + sendValue = newValue > 100 ? 100 : newValue; + if (snr != null) { + QRol.execute(sendValue, snr); + } else { + thingOffline("No serial number configured for " + rolId); + } + } } } else if (command instanceof PercentType) { PercentType p = (PercentType) command; + int pp = p.intValue(); if (p == PercentType.ZERO) { - QRol.execute(0, sn); + if (snr != null) { + QRol.execute(0, snr); + } else { + thingOffline("No serial number configured for " + rolId); + } } else { - QRol.execute(p.intValue(), sn); + if (snr != null) { + QRol.execute(pp, snr); + } else { + thingOffline("No serial number configured for " + rolId); + } } } } @@ -180,36 +252,67 @@ private void handleScreenposCommand(QbusRol QRol, Command command) { * Executes the command for screen slats position */ private void handleSlatsposCommand(QbusRol QRol, Command command) { - + @Nullable + String snr = getSN(); if (command instanceof org.openhab.core.library.types.UpDownType) { org.openhab.core.library.types.UpDownType s = (org.openhab.core.library.types.UpDownType) command; if (s == org.openhab.core.library.types.UpDownType.DOWN) { - QRol.executeSlats(0, sn); + if (snr != null) { + QRol.executeSlats(0, snr); + } else { + thingOffline("No serial number configured for " + rolId); + } } else { - QRol.executeSlats(100, sn); + if (snr != null) { + QRol.executeSlats(100, snr); + } else { + thingOffline("No serial number configured for " + rolId); + } } } else if (command instanceof IncreaseDecreaseType) { IncreaseDecreaseType s = (IncreaseDecreaseType) command; int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); - int currentValue = QRol.getState(); + Integer currentValue = QRol.getState(); int newValue; - if (s == IncreaseDecreaseType.INCREASE) { - newValue = currentValue + stepValue; - // round down to step multiple - newValue = newValue - newValue % stepValue; - QRol.executeSlats(newValue > 100 ? 100 : newValue, sn); - } else { - newValue = currentValue - stepValue; - // round up to step multiple - newValue = newValue + newValue % stepValue; - QRol.executeSlats(newValue < 0 ? 0 : newValue, sn); + int sendValue; + if (currentValue != null) { + if (s == IncreaseDecreaseType.INCREASE) { + newValue = currentValue + stepValue; + // round down to step multiple + newValue = newValue - newValue % stepValue; + sendValue = newValue > 100 ? 100 : newValue; + if (snr != null) { + QRol.executeSlats(sendValue, snr); + } else { + thingOffline("No serial number configured for " + rolId); + } + } else { + newValue = currentValue - stepValue; + // round up to step multiple + newValue = newValue + newValue % stepValue; + sendValue = newValue > 100 ? 100 : newValue; + if (snr != null) { + QRol.executeSlats(sendValue, snr); + } else { + thingOffline("No serial number configured for " + rolId); + } + } } } else if (command instanceof PercentType) { PercentType p = (PercentType) command; + int pp = p.intValue(); if (p == PercentType.ZERO) { - QRol.executeSlats(0, sn); + if (snr != null) { + QRol.executeSlats(0, snr); + } else { + thingOffline("No serial number configured for " + rolId); + } } else { - QRol.executeSlats(p.intValue(), sn); + if (snr != null) { + QRol.executeSlats(pp, snr); + } else { + thingOffline("No serial number configured for " + rolId); + } } } } @@ -219,13 +322,17 @@ private void handleSlatsposCommand(QbusRol QRol, Command command) { */ public void handleStateUpdate(QbusRol qRol) { - int rolState = qRol.getState().intValue(); - int slatState = qRol.getStateSlats().intValue(); + Integer rolState = qRol.getState(); + Integer slatState = qRol.getStateSlats(); - updateState(CHANNEL_ROLLERSHUTTER, new PercentType(rolState)); - updateState(CHANNEL_SLATS, new PercentType(slatState)); - - updateStatus(ThingStatus.ONLINE); + if (rolState != null) { + updateState(CHANNEL_ROLLERSHUTTER, new PercentType(rolState)); + updateStatus(ThingStatus.ONLINE); + } + if (slatState != null) { + updateState(CHANNEL_SLATS, new PercentType(slatState)); + updateStatus(ThingStatus.ONLINE); + } } /** @@ -240,8 +347,11 @@ protected synchronized void setConfig() { * * @return rolId */ - @SuppressWarnings("null") public int getId() { - return config.rolId; + if (config != null) { + return config.rolId; + } else { + return 0; + } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java index d70bf35bf2b85..969a425e0707a 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java @@ -12,8 +12,9 @@ */ package org.openhab.binding.qbus.internal.handler; -import static org.openhab.binding.qbus.internal.QbusBindingConstants.CHANNEL_SWITCH; -import static org.openhab.core.types.RefreshType.REFRESH; +import static org.openhab.binding.qbus.internal.QbusBindingConstants.CHANNEL_SCENE; + +import java.util.Map; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -26,8 +27,6 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusSceneHandler} is responsible for handling commands, which are @@ -39,17 +38,16 @@ @NonNullByDefault public class QbusSceneHandler extends QbusGlobalHandler { - private final Logger logger = LoggerFactory.getLogger(QbusSceneHandler.class); - public QbusSceneHandler(Thing thing) { super(thing); } - protected @Nullable QbusThingsConfig config; + protected @NonNullByDefault({}) QbusThingsConfig config; - int sceneId = 0; + int sceneId; - String sn = ""; + @Nullable + String sn; /** * Main initialization @@ -72,19 +70,51 @@ public void initialize() { return; } - QbusScene QScene = QComm.getScenes().get(sceneId); + setSN(); - sn = QBridgeHandler.getSn(); + Map sceneComm = QComm.getScene(); - if (QScene != null) { - QScene.setThingHandler(this); - handleStateUpdate(QScene); - logger.info("Scene intialized {}", sceneId); + if (sceneComm != null) { + QbusScene QScene = sceneComm.get(sceneId); + if (QScene != null) { + QScene.setThingHandler(this); + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Error while initializing the thing."); + } } else { - logger.warn("Scene not intialized {}", sceneId); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Error while initializing the thing."); + } + } + + /** + * Returns the serial number of the controller + * + * @return the serial nr + */ + public @Nullable String getSN() { + return this.sn; + } + + /** + * Sets the serial number of the controller + */ + public void setSN() { + QbusBridgeHandler QBridgeHandler = getBridgeHandler("Scene", sceneId); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "No communication with Qbus Bridge!"); + return; } + this.sn = QBridgeHandler.getSn(); + ; } + /** + * Handle the status update from the thing + */ @Override public void handleCommand(ChannelUID channelUID, Command command) { QbusCommunication QComm = getCommunication("Scene", sceneId); @@ -95,66 +125,83 @@ public void handleCommand(ChannelUID channelUID, Command command) { return; } - QbusScene QScene = QComm.getScenes().get(sceneId); + Map sceneComm = QComm.getScene(); - if (QScene == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for Scene " + sceneId); - return; - } + if (sceneComm != null) { + QbusScene QScene = sceneComm.get(sceneId); + if (QScene == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for Scene " + sceneId); + return; + } else { - scheduler.submit(() -> { - if (!QComm.communicationActive()) { - restartCommunication(QComm, "Scene", sceneId); + scheduler.submit(() -> { + if (!QComm.communicationActive()) { + restartCommunication(QComm, "Scene", sceneId); + } + + if (QComm.communicationActive()) { + + switch (channelUID.getId()) { + case CHANNEL_SCENE: + handleSwitchCommand(QScene, channelUID, command); + updateStatus(ThingStatus.ONLINE); + break; + + default: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Channel unknown " + channelUID.getId()); + } + } + }); } + } + } - if (QComm.communicationActive()) { - - if (command == REFRESH) { - handleStateUpdate(QScene); - return; - } + /** + * Method to update state of channel, called from Qbus Scene. + */ + public void handleStateUpdate(QbusScene QScene) { - switch (channelUID.getId()) { - case CHANNEL_SWITCH: - handleSwitchCommand(QScene, command); - updateStatus(ThingStatus.ONLINE); - break; + Integer sceneState = QScene.getState(); + if (sceneState != null) { + updateState(CHANNEL_SCENE, (sceneState == 0) ? OnOffType.OFF : OnOffType.ON); + updateStatus(ThingStatus.ONLINE); + } + } - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Channel unknown " + channelUID.getId()); - } - } - }); + /** + * + * @param message + */ + public void thingOffline(String message) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); } /** * Executes the scene command */ - private void handleSwitchCommand(QbusScene QScene, Command command) { - + private void handleSwitchCommand(QbusScene QScene, ChannelUID channelUID, Command command) { + @Nullable + String snr = getSN(); if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; if (s == OnOffType.OFF) { - QScene.execute(0, sn); + if (snr != null) { + QScene.execute(0, snr); + } else { + thingOffline("No serial number configured for " + sceneId); + } } else { - QScene.execute(100, sn); + if (snr != null) { + QScene.execute(100, snr); + } else { + thingOffline("No serial number configured for " + sceneId); + } } } } - /** - * Method to update state of channel, called from Qbus Scene. - */ - public void handleStateUpdate(QbusScene QScene) { - - int sceneState = QScene.getState(); - - updateState(CHANNEL_SWITCH, (sceneState == 0) ? OnOffType.OFF : OnOffType.ON); - updateStatus(ThingStatus.ONLINE); - } - /** * Read the configuration */ @@ -167,8 +214,11 @@ protected synchronized void setConfig() { * * @return sceneId */ - @SuppressWarnings("null") public int getId() { - return config.sceneId; + if (config != null) { + return config.sceneId; + } else { + return 0; + } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java index 12ae5771d11ae..2b05ec07f59bb 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java @@ -16,6 +16,8 @@ import static org.openhab.core.library.unit.SIUnits.CELSIUS; import static org.openhab.core.types.RefreshType.REFRESH; +import java.util.Map; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.QbusBridgeHandler; @@ -28,8 +30,6 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.types.Command; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusThermostatHandler} is responsible for handling commands, which are @@ -41,17 +41,16 @@ @NonNullByDefault public class QbusThermostatHandler extends QbusGlobalHandler { - private final Logger logger = LoggerFactory.getLogger(QbusThermostatHandler.class); - public QbusThermostatHandler(Thing thing) { super(thing); } - protected @Nullable QbusThingsConfig config; + protected @NonNullByDefault({}) QbusThingsConfig config; - int thermostatId = 0; + int thermostatId; - String sn = ""; + @Nullable + String sn; /** * Main initialization @@ -63,6 +62,8 @@ public void initialize() { QbusCommunication QComm = getCommunication("Thermostat", thermostatId); if (QComm == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "No communication with Qbus Bridge!"); return; } @@ -71,16 +72,45 @@ public void initialize() { return; } - QbusThermostat QThermostat = QComm.getThermostats().get(thermostatId); + setSN(); + + Map thermostatComm = QComm.getThermostat(); - sn = QBridgeHandler.getSn(); + if (thermostatComm != null) { + QbusThermostat QThermostat = thermostatComm.get(thermostatId); + if (QThermostat != null) { + QThermostat.setThingHandler(this); + handleStateUpdate(QThermostat); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Error while initializing the thing."); + } + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "Error while initializing the thing."); + } + } + + /** + * Returns the serial number of the controller + * + * @return the serial nr + */ + public @Nullable String getSN() { + return this.sn; + } - if (QThermostat != null) { - QThermostat.setThingHandler(this); - handleStateUpdate(QThermostat); - logger.info("Thermostat intialized {}", thermostatId); + /** + * Sets the serial number of the controller + */ + public void setSN() { + QbusBridgeHandler QBridgeHandler = getBridgeHandler("Thermostat", thermostatId); + if (QBridgeHandler == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, + "No communication with Qbus Bridge!"); + return; } else { - logger.info("Thermostat not intialized {}", thermostatId); + this.sn = QBridgeHandler.getSn(); } } @@ -98,56 +128,78 @@ public void handleCommand(ChannelUID channelUID, Command command) { return; } - QbusThermostat QThermostat = QComm.getThermostats().get(thermostatId); + Map thermostatComm = QComm.getThermostat(); - if (QThermostat == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for Scene " + thermostatId); - return; - } + if (thermostatComm != null) { - scheduler.submit(() -> { - if (!QComm.communicationActive()) { - restartCommunication(QComm, "Thermostat", thermostatId); - } + QbusThermostat QThermostat = thermostatComm.get(thermostatId); + + if (QThermostat == null) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge communication not initialized when trying to execute command for Scene " + + thermostatId); + return; + } else { + + scheduler.submit(() -> { + if (!QComm.communicationActive()) { + restartCommunication(QComm, "Thermostat", thermostatId); + } - if (QComm.communicationActive()) { + if (QComm.communicationActive()) { - if (command == REFRESH) { - handleStateUpdate(QThermostat); - return; - } + if (command == REFRESH) { + handleStateUpdate(QThermostat); + return; + } - switch (channelUID.getId()) { - case CHANNEL_MEASURED: - updateStatus(ThingStatus.ONLINE); - break; + switch (channelUID.getId()) { + case CHANNEL_MEASURED: + updateStatus(ThingStatus.ONLINE); + break; - case CHANNEL_MODE: - handleModeCommand(QThermostat, command); - updateStatus(ThingStatus.ONLINE); - break; + case CHANNEL_MODE: + handleModeCommand(QThermostat, command); + updateStatus(ThingStatus.ONLINE); + break; - case CHANNEL_SETPOINT: - handleSetpointCommand(QThermostat, command); - updateStatus(ThingStatus.ONLINE); - break; + case CHANNEL_SETPOINT: + handleSetpointCommand(QThermostat, command); + updateStatus(ThingStatus.ONLINE); + break; - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Channel unknown " + channelUID.getId()); - } + default: + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, + "Channel unknown " + channelUID.getId()); + } + } + }); } - }); + } + } + + /** + * Puts thing offline + * + * @param message + */ + public void thingOffline(String message) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); } /** * Executes the Mode command */ private void handleModeCommand(QbusThermostat QThermostat, Command command) { - + @Nullable + String snr = getSN(); if (command instanceof DecimalType) { - QThermostat.executeMode(((DecimalType) command).intValue(), sn); + int mode = ((DecimalType) command).intValue(); + if (snr != null) { + QThermostat.executeMode(mode, snr); + } else { + thingOffline("No serial number configured for " + thermostatId); + } } } @@ -155,10 +207,16 @@ private void handleModeCommand(QbusThermostat QThermostat, Command command) { * Executes the Setpoint command */ private void handleSetpointCommand(QbusThermostat QThermostat, Command command) { - + @Nullable + String snr = getSN(); if (command instanceof QuantityType) { QuantityType s = (QuantityType) command; - QThermostat.executeSetpoint(s.doubleValue(), sn); + double sp = s.doubleValue(); + if (snr != null) { + QThermostat.executeSetpoint(sp, snr); + } else { + thingOffline("No serial number configured for " + thermostatId); + } } } @@ -191,8 +249,11 @@ protected synchronized void setConfig() { * * @return dimmerId */ - @SuppressWarnings("null") public int getId() { - return config.thermostatId; + if (config != null) { + return config.thermostatId; + } else { + return 0; + } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java index 6ea8b74a1185e..7299c1da1dd4e 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java @@ -15,8 +15,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusBistabielHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusBistabiel} class represents the Qbus BISTABIEL output. @@ -27,13 +25,13 @@ @NonNullByDefault public final class QbusBistabiel { - private final Logger logger = LoggerFactory.getLogger(QbusBistabiel.class); - @Nullable private QbusCommunication QComm; - private String id = ""; - private Integer state = 0; + private String id; + + @Nullable + private Integer state; @Nullable private QbusBistabielHandler thingHandler; @@ -69,8 +67,12 @@ public void setQComm(QbusCommunication QComm) { * * @return bistabiel state */ - public Integer getState() { - return this.state; + public @Nullable Integer getState() { + if (this.state != null) { + return this.state; + } else { + return null; + } } /** @@ -82,7 +84,6 @@ void setState(int state) { this.state = state; QbusBistabielHandler handler = this.thingHandler; if (handler != null) { - logger.info("Update bistabiel channel state for {} with {}", id, state); handler.handleStateUpdate(this); } } @@ -91,11 +92,7 @@ void setState(int state) { * Sends bistabiel to Qbus. */ public void execute(int value, String sn) { - - logger.info("Execute bistabiel for {}", this.id); - QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeBistabiel").withId(this.id).withState(value); - QbusCommunication comm = QComm; if (comm != null) { comm.sendMessage(QCmd); diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java index f83dc99efd872..c8db92d174104 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java @@ -16,8 +16,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusCO2Handler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusCO2} class represents the action Qbus CO2 output. @@ -28,18 +26,12 @@ @NonNullByDefault public final class QbusCO2 { - private final Logger logger = LoggerFactory.getLogger(QbusCO2.class); - - private String co2Id; - private Integer co2State = 0; + @Nullable + private Integer state; @Nullable private QbusCO2Handler thingHandler; - QbusCO2(String co2Id) { - this.co2Id = co2Id; - } - /** * This method should be called if the ThingHandler for the thing corresponding to this CO2 is initialized. * It keeps a record of the thing handler in this object so the thing can be updated when @@ -56,8 +48,12 @@ public void setThingHandler(QbusCO2Handler handler) { * * @return CO2 state */ - public Integer getState() { - return this.co2State; + public @Nullable Integer getState() { + if (this.state != null) { + return this.state; + } else { + return null; + } } /** @@ -66,10 +62,9 @@ public Integer getState() { * @param CO2 state */ public void setState(Integer co2) { - this.co2State = co2; + this.state = co2; QbusCO2Handler handler = thingHandler; if (handler != null) { - logger.info("Update channel state for {} with {}", co2Id, co2State); handler.handleStateUpdate(this); } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java index fa222a68b1b7d..a042f8f20fea1 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java @@ -65,9 +65,9 @@ public final class QbusCommunication { private Gson gsonOut = new Gson(); private Gson gsonIn; + @Nullable + private String CTD; - private String CTD = ""; - // @Nullable private Boolean CTDConnected = false; private final Map bistabiel = new HashMap<>(); @@ -102,6 +102,7 @@ public QbusCommunication() { public synchronized void startCommunication() { QbusBridgeHandler handler = bridgeCallBack; + CTDConnected = false; try { for (int i = 1; qEventsRunning && (i <= 5); i++) { @@ -117,17 +118,22 @@ public synchronized void startCommunication() { throw new IOException(); } - InetAddress addr = null; - addr = InetAddress.getByName(handler.getAddress()); - int port = handler.getPort(); + InetAddress addr = InetAddress.getByName(handler.getAddress()); + Integer port = handler.getPort(); + + if (port != null) { + Socket socket = new Socket(addr, port); + qSocket = socket; + qOut = new PrintWriter(socket.getOutputStream(), true); + qIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); + logger.info("Connected via local port {} from thread {}", socket.getLocalPort(), + Thread.currentThread().getId()); + } else { + return; + } - Socket socket = new Socket(addr, port); - qSocket = socket; - qOut = new PrintWriter(socket.getOutputStream(), true); - qIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); - logger.info("Connected via local port {} from thread {}", socket.getLocalPort(), - Thread.currentThread().getId()); - CTDConnected = false; + setSN(); + getSN(); // Connect to Qbus server Connect(); @@ -193,7 +199,7 @@ public synchronized void restartCommunication() { */ public boolean communicationActive() { - return (qSocket != null); + return qSocket != null; } /** @@ -203,7 +209,7 @@ public boolean communicationActive() { */ public boolean clientConnected() { - return (CTDConnected); + return CTDConnected; } /** @@ -292,15 +298,25 @@ synchronized void sendMessage(Object qMessage) { * Called by other methods to Qbus server and read response */ private void sendAndReadMessage(String command) throws IOException, InterruptedException { - QbusMessageCmd qCmd = new QbusMessageCmd(CTD, command); + @Nullable + String snr = getSN(); + if (snr != null) { - sendMessage(qCmd); + QbusMessageCmd qCmd = new QbusMessageCmd(snr, command); - BufferedReader reader = qIn; - if (reader == null) { - throw new IOException("Cannot read from socket, reader not connected."); + sendMessage(qCmd); + + BufferedReader reader = qIn; + if (reader == null) { + throw new IOException("Cannot read from socket, reader not connected."); + } + readMessage(reader.readLine()); + } else { + QbusBridgeHandler handler = bridgeCallBack; + if (handler != null) { + handler.bridgeOffline("No serial nr defined"); + } } - readMessage(reader.readLine()); } /** @@ -309,9 +325,9 @@ private void sendAndReadMessage(String command) throws IOException, InterruptedE * @param qMessage message read from Qbus. */ private void readMessage(String qMessage) { - String confsn = ""; String cmd = ""; String CTD = ""; + String sn = null; QbusMessageBase qMessageGson; qMessageGson = gsonIn.fromJson(qMessage, QbusMessageBase.class); @@ -321,56 +337,58 @@ private void readMessage(String qMessage) { } if (bridgeCallBack != null) { - confsn = bridgeCallBack.getSn(); + sn = bridgeCallBack.getSn(); } - try { - if (Integer.parseInt(confsn) == Integer.parseInt(CTD) && qMessageGson != null) { - // Get the compatible outputs from the Qbus server - if ("returnBistabiel".equals(cmd)) { - cmdListBistabiel(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if ("returnDimmer".equals(cmd)) { - cmdListDimmers(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if (("returnThermostat").equals(cmd)) { - cmdListThermostat(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if (("returnScene").equals(cmd)) { - cmdlistscenes(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if (("returnCo2").equals(cmd)) { - cmdlistco2(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if (("returnRol02p").equals(cmd)) { - cmdlistrol(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if (("returnSlat").equals(cmd)) { - cmdlistrolslats(((QbusMessageListMap) qMessageGson).getOutputs()); - } + if (sn != null && CTD != null) { + try { - // Incoming commands from Qbus Client to openHAB (event) - else if ("updateBistabiel".equals(cmd)) { - updateBistabiel(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if ("updateDimmer".equals(cmd)) { - eventListDimmers(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if ("updateThermostat".equals(cmd)) { - eventListThermostat(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if ("updateScene".equals(cmd)) { - eventListScenes(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if ("updateCo2".equals(cmd)) { - eventListCO2(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if ("updateRol02p".equals(cmd)) { - eventListRol(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if ("updateRol02pSlat".equals(cmd)) { - eventListRolslats(((QbusMessageListMap) qMessageGson).getOutputs()); - } + if (Integer.parseInt(sn) == Integer.parseInt(CTD) && qMessageGson != null) { + // Get the compatible outputs from the Qbus server + if ("returnBistabiel".equals(cmd)) { + cmdListBistabiel(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if ("returnDimmer".equals(cmd)) { + cmdListDimmers(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if (("returnThermostat").equals(cmd)) { + cmdListThermostat(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if (("returnScene").equals(cmd)) { + cmdlistscenes(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if (("returnCo2").equals(cmd)) { + cmdlistco2(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if (("returnRol02p").equals(cmd)) { + cmdlistrol(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if (("returnSlat").equals(cmd)) { + cmdlistrolslats(((QbusMessageListMap) qMessageGson).getOutputs()); + } + + // Incoming commands from Qbus Client to openHAB (event) + else if ("updateBistabiel".equals(cmd)) { + updateBistabiel(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if ("updateDimmer".equals(cmd)) { + updateDimmers(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if ("updateThermostat".equals(cmd)) { + updateThermostat(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if ("updateCo2".equals(cmd)) { + updateCO2(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if ("updateRol02p".equals(cmd)) { + updateRol(((QbusMessageListMap) qMessageGson).getOutputs()); + } else if ("updateRol02pSlat".equals(cmd)) { + updateRolslats(((QbusMessageListMap) qMessageGson).getOutputs()); + } - // Incomming commands from Qbus server to verify the client connection - else if ("disconnect".equals(cmd)) { - eventDisconnect(); - } else if ("notConnected".equals(cmd)) { - noConnection(); - } else if ("connected".equals(cmd)) { - connection(); + // Incomming commands from Qbus server to verify the client connection + else if ("disconnect".equals(cmd)) { + eventDisconnect(); + } else if ("notConnected".equals(cmd)) { + noConnection(); + } else if ("connected".equals(cmd)) { + connection(); + } } + + } catch (JsonParseException e) { + logger.warn("Not acted on unsupported json {}", qMessage); } - } catch (JsonParseException e) { - logger.warn("Not acted on unsupported json {}", qMessage); } } @@ -391,21 +409,15 @@ else if ("disconnect".equals(cmd)) { */ private void initialize() throws IOException, InterruptedException { + if (bridgeCallBack != null) { if (CTDConnected) { - logger.info("Requesting Bistabiel outputs from client"); sendAndReadMessage("getBistabiel"); - logger.info("Requesting Scenes from client"); sendAndReadMessage("getScene"); - logger.info("Requesting Dimmers from client"); sendAndReadMessage("getDimmer"); - logger.info("Requesting Shutters from client"); sendAndReadMessage("getRol02p"); - logger.info("Requesting Shutters whith slat control from client"); sendAndReadMessage("getRol02pSlat"); - logger.info("Requesting Thermostats from client"); sendAndReadMessage("getThermostat"); - logger.info("Requesting CO2 outputs from client"); sendAndReadMessage("getCo2"); } else { CTDConnected = false; @@ -423,6 +435,17 @@ private void initialize() throws IOException, InterruptedException { } } + public @Nullable String getSN() { + return this.CTD; + } + + public void setSN() { + QbusBridgeHandler QBridgeHandler = bridgeCallBack; + if (QBridgeHandler != null) { + this.CTD = QBridgeHandler.getSn(); + } + } + /** * Initial connection to Qbus Server to open a communication channel * @@ -430,18 +453,27 @@ private void initialize() throws IOException, InterruptedException { * */ private void Connect() throws InterruptedException, IOException { - if (bridgeCallBack != null) { - CTD = bridgeCallBack.getSn(); - } - QbusMessageCmd QCmd = new QbusMessageCmd(CTD, "openHAB"); + @Nullable + String snr = getSN(); - sendMessage(QCmd); - BufferedReader reader = qIn; + if (snr != null) { + QbusMessageCmd QCmd = new QbusMessageCmd(snr, "openHAB"); + + sendMessage(QCmd); + BufferedReader reader = qIn; - if (reader == null) { - throw new IOException("Cannot read from socket, reader not connected."); + if (reader == null) { + throw new IOException("Cannot read from socket, reader not connected."); + } + readMessage(reader.readLine()); + } else { + QbusBridgeHandler handler = bridgeCallBack; + if (handler != null) { + handler.bridgeOffline("No serial nr defined"); + } } - readMessage(reader.readLine()); + + return; } /** @@ -449,24 +481,22 @@ private void Connect() throws InterruptedException, IOException { * * @param data */ - @SuppressWarnings("null") private void cmdListBistabiel(@Nullable List> outputs) { if (outputs != null) { for (Map bistabiel : outputs) { String idStr = bistabiel.get("id"); String stateStr = bistabiel.get("state"); - if (idStr != null && stateStr != null) { int id = Integer.parseInt(idStr); Integer state = Integer.parseInt(stateStr); + QbusBistabiel qBistabiel = new QbusBistabiel(idStr); if (!this.bistabiel.containsKey(id)) { - QbusBistabiel qBistabiel = new QbusBistabiel(idStr); qBistabiel.setState(state); qBistabiel.setQComm(this); this.bistabiel.put(id, qBistabiel); - this.bistabiel.get(id).setState(state); + qBistabiel.setState(state); } else { - this.bistabiel.get(id).setState(state); + qBistabiel.setState(state); } } else { logger.error("Error in json for BistabBistabiel/Timers/Monos/Intervals"); @@ -501,7 +531,6 @@ private void cmdlistscenes(@Nullable List> outputs) { * * @param outputs */ - @SuppressWarnings("null") private void cmdListDimmers(@Nullable List> outputs) { if (outputs != null) { for (Map dimmer : outputs) { @@ -510,13 +539,13 @@ private void cmdListDimmers(@Nullable List> outputs) { if (idStr != null && stateStr != null) { int id = Integer.parseInt(idStr); Integer state = Integer.parseInt(stateStr); + QbusDimmer qDimmer = new QbusDimmer(idStr); if (!this.dimmer.containsKey(id)) { - QbusDimmer qDimmer = new QbusDimmer(idStr); - qDimmer.updateState(state); qDimmer.setQComm(this); this.dimmer.put(id, qDimmer); + qDimmer.updateState(state); } else { - this.dimmer.get(id).updateState(state); + qDimmer.updateState(state); } } else { logger.error("Error in json for Dimmer"); @@ -530,7 +559,6 @@ private void cmdListDimmers(@Nullable List> outputs) { * * @param data */ - @SuppressWarnings("null") private void cmdlistrol(@Nullable List> outputs) { if (outputs != null) { for (Map rol : outputs) { @@ -539,13 +567,13 @@ private void cmdlistrol(@Nullable List> outputs) { if (idStr != null && stateStr != null) { int id = Integer.parseInt(idStr); Integer rolpos = Integer.valueOf(stateStr); + QbusRol Qrol = new QbusRol(idStr); if (!this.rol.containsKey(id)) { - QbusRol Rol = new QbusRol(idStr); - Rol.setQComm(this); - this.rol.put(id, Rol); - this.rol.get(id).setState(rolpos); + Qrol.setQComm(this); + this.rol.put(id, Qrol); + Qrol.setState(rolpos); } else { - this.rol.get(id).setState(rolpos); + Qrol.setState(rolpos); } } else { logger.error("Error in json for ROL02P"); @@ -559,29 +587,26 @@ private void cmdlistrol(@Nullable List> outputs) { * * @param data */ - @SuppressWarnings("null") private void cmdlistrolslats(@Nullable List> outputs) { + logger.debug("Qbus: ROL02PSLATS received from Qbus server"); if (outputs != null) { for (Map rol : outputs) { - String idStr = rol.get("id"); String rolPos = rol.get("rolPos"); String slatPos = rol.get("slatPos"); if (idStr != null && rolPos != null && slatPos != null) { int id = Integer.parseInt(idStr); - Integer rolpos = Integer.valueOf(rolPos); - Integer rolposslats = Integer.valueOf(slatPos); - rolpos = Integer.valueOf(rolpos); - rolposslats = Integer.valueOf(rolposslats); + Integer rolpos = Integer.parseInt(rolPos); + Integer rolposslats = Integer.parseInt(slatPos); + QbusRol Qrol = new QbusRol(idStr); if (!this.rol.containsKey(id)) { - QbusRol Rol = new QbusRol(idStr); - Rol.setQComm(this); - this.rol.put(id, Rol); - this.rol.get(id).setState(rolpos); - this.rol.get(id).setSlats(rolposslats); + Qrol.setQComm(this); + this.rol.put(id, Qrol); + Qrol.setState(rolpos); + Qrol.setSlats(rolposslats); } else { - this.rol.get(id).setState(rolpos); - this.rol.get(id).setSlats(rolposslats); + Qrol.setState(rolpos); + Qrol.setSlats(rolposslats); } } else { logger.error("Error in json for ROL02P_Slats"); @@ -595,25 +620,21 @@ private void cmdlistrolslats(@Nullable List> outputs) { * * @param data */ - @SuppressWarnings("null") private void cmdlistco2(@Nullable List> outputs) { if (outputs != null) { for (Map co2 : outputs) { String idStr = co2.get("id"); String stateStr = co2.get("state"); if (idStr != null && stateStr != null) { - int id = Integer.parseInt(idStr); int state = Integer.parseInt(stateStr); - + QbusCO2 CO2 = new QbusCO2(); if (!this.co2.containsKey(id)) { - QbusCO2 CO2 = new QbusCO2(idStr); this.co2.put(id, CO2); - this.co2.get(id).setState(state); + CO2.setState(state); } else { - this.co2.get(id).setState(state); + CO2.setState(state); } - } else { logger.error("Error in json for CO2"); } @@ -627,7 +648,6 @@ private void cmdlistco2(@Nullable List> outputs) { * * @param data */ - @SuppressWarnings("null") private void cmdListThermostat(@Nullable List> outputs) { if (outputs != null) { for (Map thermostat : outputs) { @@ -640,14 +660,13 @@ private void cmdListThermostat(@Nullable List> outputs) { Double measured = Double.valueOf(measuredStr); Double setpoint = Double.valueOf(setpointStr); Integer mode = Integer.valueOf(modeStr); - + QbusThermostat qThermostat = new QbusThermostat(idStr); if (!this.thermostat.containsKey(id)) { - QbusThermostat qThermostat = new QbusThermostat(idStr); qThermostat.updateState(measured, setpoint, mode); qThermostat.setQComm(this); this.thermostat.put(id, qThermostat); } else { - this.thermostat.get(id).updateState(measured, setpoint, mode); + qThermostat.updateState(measured, setpoint, mode); } } else { logger.error("Error in json for Thermostats"); @@ -661,44 +680,22 @@ private void cmdListThermostat(@Nullable List> outputs) { * * @param data */ - @SuppressWarnings("null") private void updateBistabiel(List> output) { for (Map bistabiel : output) { String idStr = bistabiel.get("id"); String stateStr = bistabiel.get("state"); if (idStr != null && stateStr != null) { - int id = Integer.valueOf(idStr); - int value1 = Integer.valueOf(stateStr); - if (!this.bistabiel.containsKey(id)) { - logger.warn("Bistabiel in controller not known {}", id); - return; - } - logger.info("Event execute bistabiel {} with state {}", id, value1); - this.bistabiel.get(id).setState(value1); - } - } - } - - /** - * Event on incoming Scene updates - * - * @param data - */ - @SuppressWarnings("null") - private void eventListScenes(List> data) { - for (Map scene : data) { - String idStr = scene.get("id"); - String value1Str = scene.get("state"); - if (idStr != null && value1Str != null) { - int id = Integer.valueOf(idStr); - int value1 = Integer.valueOf(value1Str); - if (!this.scene.containsKey(id)) { - logger.warn("Scene in controller not known {}", id); - return; + int id = Integer.parseInt(idStr); + int value1 = Integer.parseInt(stateStr); + QbusBistabiel Bistabiel = this.bistabiel.get(id); + if (Bistabiel != null) { + if (!this.bistabiel.containsKey(id)) { + logger.warn("Bistabiel in controller not known {}", id); + return; + } + logger.info("Event execute bistabiel {} with state {}", id, value1); + Bistabiel.setState(value1); } - - logger.info("Event execute scene {} with state {}", id, value1); - this.scene.get(id).setState(value1); } } } @@ -708,22 +705,22 @@ private void eventListScenes(List> data) { * * @param data */ - @SuppressWarnings("null") - private void eventListDimmers(List> data) { + private void updateDimmers(List> data) { for (Map dimmer : data) { String idStr = dimmer.get("id"); - String value1Str = dimmer.get("state"); - if (idStr != null && value1Str != null) { + String stateStr = dimmer.get("state"); + if (idStr != null && stateStr != null) { int id = Integer.valueOf(idStr); - int value1 = Integer.valueOf(value1Str); - + int value = Integer.valueOf(stateStr); + QbusDimmer Qdimmer = this.dimmer.get(id); if (!this.dimmer.containsKey(id)) { logger.warn("Dimmer in controller not known {}", id); return; } - - logger.info("Event execute dimmer {} with state {}", id, value1); - this.dimmer.get(id).setState(value1); + if (Qdimmer != null) { + logger.info("Event execute dimmer {} with state {}", id, value); + Qdimmer.setState(value); + } } } } @@ -733,22 +730,22 @@ private void eventListDimmers(List> data) { * * @param data */ - @SuppressWarnings("null") - private void eventListRol(List> data) { + private void updateRol(List> data) { for (Map rol : data) { - String idStr = rol.get("id"); - String value1Str = rol.get("value1Str"); - if (idStr != null && value1Str != null) { + String posStr = rol.get("value1Str"); + if (idStr != null && posStr != null) { int id = Integer.valueOf(idStr); - int value1 = Integer.valueOf(value1Str); - + int pos = Integer.valueOf(posStr); + QbusRol Qrol = this.rol.get(id); if (!this.rol.containsKey(id)) { logger.warn("Rol02p in controller not known {}", id); return; } - logger.info("Event execute Rol02P {} with pos {}", id, value1); - this.rol.get(id).setState(value1); + if (Qrol != null) { + logger.info("Event execute Rol02P {} with pos {}", id, pos); + Qrol.setState(pos); + } } } @@ -759,24 +756,25 @@ private void eventListRol(List> data) { * * @param data */ - @SuppressWarnings("null") - private void eventListRolslats(List> data) { + private void updateRolslats(List> data) { for (Map rol : data) { String idStr = rol.get("id"); - String value1Str = rol.get("pos"); - String value2Str = rol.get("slats"); - if (idStr != null && value1Str != null && value2Str != null) { + String posStr = rol.get("pos"); + String slatsStr = rol.get("slats"); + if (idStr != null && posStr != null && slatsStr != null) { int id = Integer.valueOf(idStr); - int value1 = Integer.valueOf(value1Str); - int value2 = Integer.valueOf(value2Str); + int pos = Integer.valueOf(posStr); + int slats = Integer.valueOf(slatsStr); + QbusRol Qrol = this.rol.get(id); if (!this.rol.containsKey(id)) { logger.warn("Rol02p in controller not known {}", id); return; } - - logger.info("Event execute ROL02P_Slats {} with pos {} and slats {}", id, value1, value2); - this.rol.get(id).setState(value1); - this.rol.get(id).setSlats(value2); + if (Qrol != null) { + logger.info("Event execute ROL02P_Slats {} with pos {} and slats {}", id, pos, slats); + Qrol.setState(pos); + Qrol.setSlats(slats); + } } } } @@ -786,8 +784,7 @@ private void eventListRolslats(List> data) { * * @param data */ - @SuppressWarnings("null") - private void eventListThermostat(List> data) { + private void updateThermostat(List> data) { for (Map thermostat : data) { String idStr = thermostat.get("id"); String measuredStr = thermostat.get("measured"); @@ -798,15 +795,16 @@ private void eventListThermostat(List> data) { Double measured = Double.valueOf(measuredStr); Double setpoint = Double.valueOf(setpointdStr); Integer mode = Integer.valueOf(modedStr); - + QbusThermostat Qthermostat = this.thermostat.get(id); if (!this.thermostat.containsKey(id)) { logger.warn("Thermostat in controller not known {}", id); return; } - - logger.info("Event execute thermostat {} with measured {}, setpoint {}, mode {}", id, measured, - setpoint, mode); - this.thermostat.get(id).updateState(measured, setpoint, mode); + if (Qthermostat != null) { + logger.info("Event execute thermostat {} with measured {}, setpoint {}, mode {}", id, measured, + setpoint, mode); + Qthermostat.updateState(measured, setpoint, mode); + } } } } @@ -816,26 +814,30 @@ private void eventListThermostat(List> data) { * * @param data */ - @SuppressWarnings("null") - private void eventListCO2(List> data) { + private void updateCO2(List> data) { for (Map co2 : data) { String idStr = co2.get("id"); String value1Str = co2.get("value1Str"); if (idStr != null && value1Str != null) { int id = Integer.valueOf(idStr); int value1 = Integer.valueOf(value1Str); + QbusCO2 Qco2 = this.co2.get(id); if (!this.co2.containsKey(id)) { logger.warn("Co2 in controller not known {}", id); return; } - logger.info("Event execute co2 {} with state {}", id, value1); - this.co2.get(id).setState(value1); + if (Qco2 != null) { + logger.info("Event execute co2 {} with state {}", id, value1); + Qco2.setState(value1); + } } } } + /** + * Put Bridge offline when QbusClient disconnects + */ private void eventDisconnect() { - logger.info("Disconnect received from client. Putting Bridge offline"); CTDConnected = false; QbusBridgeHandler handler = bridgeCallBack; if (handler != null) { @@ -843,8 +845,10 @@ private void eventDisconnect() { } } + /** + * Put Bridge offline when there is no connection from the QbusClient + */ private void noConnection() { - logger.info("No CTD connected to Qbus server"); CTDConnected = false; QbusBridgeHandler handler = bridgeCallBack; if (handler != null) { @@ -852,8 +856,10 @@ private void noConnection() { } } + /** + * Set connection state true if there is a connection from QbusClient + */ private void connection() { - logger.info("CTD connected to Qbus server"); CTDConnected = true; } @@ -862,7 +868,7 @@ private void connection() { * * @return */ - public Map getBistabiel() { + public @Nullable Map getBistabiel() { return this.bistabiel; } @@ -871,7 +877,7 @@ public Map getBistabiel() { * * @return */ - public Map getScenes() { + public @Nullable Map getScene() { return this.scene; } @@ -880,7 +886,7 @@ public Map getScenes() { * * @return */ - public Map getDimmer() { + public @Nullable Map getDimmer() { return this.dimmer; } @@ -889,7 +895,7 @@ public Map getDimmer() { * * @return */ - public Map getRol() { + public @Nullable Map getRol() { return this.rol; } @@ -898,7 +904,7 @@ public Map getRol() { * * @return */ - public Map getThermostats() { + public @Nullable Map getThermostat() { return this.thermostat; } @@ -907,7 +913,7 @@ public Map getThermostats() { * * @return */ - public Map getCo2() { + public @Nullable Map getCo2() { return this.co2; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java index 39589001b9444..60c0e5759f68a 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java @@ -15,8 +15,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusDimmerHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusDimmer} class represents the action Qbus Dimmer output. @@ -27,13 +25,13 @@ @NonNullByDefault public final class QbusDimmer { - private final Logger logger = LoggerFactory.getLogger(QbusDimmer.class); - @Nullable private QbusCommunication QComm; - private String id = ""; - private Integer state = 0; + private String id; + + @Nullable + private Integer state; @Nullable private QbusDimmerHandler thingHandler; @@ -52,7 +50,6 @@ public void updateState(Integer state) { QbusDimmerHandler handler = thingHandler; if (handler != null) { - logger.info("Qbus: update channels for {}", id); handler.handleStateUpdate(this); } } @@ -84,8 +81,12 @@ public void setQComm(QbusCommunication QComm) { * * @return dimmer state */ - public Integer getState() { - return this.state; + public @Nullable Integer getState() { + if (this.state != null) { + return this.state; + } else { + return null; + } } /** @@ -97,7 +98,6 @@ void setState(int state) { this.state = state; QbusDimmerHandler handler = thingHandler; if (handler != null) { - logger.info("Update channel state for {} with {}", id, state); handler.handleStateUpdate(this); } } @@ -106,10 +106,7 @@ void setState(int state) { * Sends Dimmer state to Qbus. */ public void execute(int percent, String sn) { - logger.info("Execute dimmer for {} ", this.id); - QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeDimmer").withId(this.id).withState(percent); - QbusCommunication comm = QComm; if (comm != null) { comm.sendMessage(QCmd); diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java index d14cc63ba1ba7..7ec1d2fdf304e 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java @@ -12,6 +12,9 @@ */ package org.openhab.binding.qbus.internal.protocol; +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + /** * Class {@link QbusMessageBase} used as base class for output from gson for cmd or event feedback from the Qbus server. * This class only contains the common base fields required for the deserializer @@ -22,16 +25,18 @@ * @author Koen Schockaert - Initial Contribution */ +@NonNullByDefault abstract class QbusMessageBase { - private String CTD; - protected String cmd; - protected String id; - protected Integer state; - protected Integer mode; - protected Double setpoint; - protected Integer slatState; + private @Nullable String CTD; + protected @Nullable String cmd; + protected @Nullable String id; + protected @Nullable Integer state; + protected @Nullable Integer mode; + protected @Nullable Double setpoint; + protected @Nullable Integer slatState; + @Nullable String getSn() { return this.CTD; } @@ -40,6 +45,7 @@ void setSn(String CTD) { this.CTD = CTD; } + @Nullable String getCmd() { return this.cmd; } @@ -48,6 +54,7 @@ void setCmd(String cmd) { this.cmd = cmd; } + @Nullable public String getId() { return id; } @@ -56,7 +63,8 @@ public void setId(String id) { this.id = id; } - public int getState() { + @Nullable + public Integer getState() { return state; } @@ -64,7 +72,8 @@ public void setState(int state) { this.state = state; } - public int getMode() { + @Nullable + public Integer getMode() { return mode; } @@ -72,6 +81,7 @@ public void setMode(int mode) { this.mode = mode; } + @Nullable public Double getSetPoint() { return setpoint; } @@ -80,7 +90,8 @@ public void setSetPoint(Double setpoint) { this.setpoint = setpoint; } - public int getSlatState() { + @Nullable + public Integer getSlatState() { return slatState; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java index 9d63d9ecf7f74..3b9389e999f22 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java @@ -15,8 +15,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusRolHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusRol} class represents the action Qbus Shutter/Slats output. @@ -27,14 +25,15 @@ @NonNullByDefault public final class QbusRol { - private final Logger logger = LoggerFactory.getLogger(QbusRol.class); - @Nullable private QbusCommunication QComm; - private String id = ""; - private Integer state = 0; - private Integer slats = 0; + private String id; + + @Nullable + private Integer state; + @Nullable + private Integer slats; @Nullable private QbusRolHandler thingHandler; @@ -71,8 +70,12 @@ public void setQComm(QbusCommunication QComm) { * * @return shutter state */ - public Integer getState() { - return this.state; + public @Nullable Integer getState() { + if (this.state != null) { + return this.state; + } else { + return null; + } } /** @@ -80,8 +83,12 @@ public Integer getState() { * * @return slats state */ - public Integer getStateSlats() { - return this.slats; + public @Nullable Integer getStateSlats() { + if (this.slats != null) { + return this.slats; + } else { + return null; + } } /** @@ -89,11 +96,10 @@ public Integer getStateSlats() { * * @param shutter state */ - public void setState(Integer Slats) { - this.state = Slats; - QbusRolHandler handler = thingHandler; + public void setState(Integer stat) { + this.state = stat; + QbusRolHandler handler = this.thingHandler; if (handler != null) { - logger.info("Update channel shutter for {} with {}", id, state); handler.handleStateUpdate(this); } } @@ -105,9 +111,8 @@ public void setState(Integer Slats) { */ public void setSlats(Integer Slats) { this.slats = Slats; - QbusRolHandler handler = thingHandler; + QbusRolHandler handler = this.thingHandler; if (handler != null) { - logger.info("Update channel slats for {} with {}", id, slats); handler.handleStateUpdate(this); } } @@ -116,10 +121,7 @@ public void setSlats(Integer Slats) { * Sends shutter to Qbus. */ public void execute(int value, String sn) { - logger.info("Execute position for {}", this.id); - QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeStore").withId(this.id).withState(value); - QbusCommunication comm = QComm; if (comm != null) { comm.sendMessage(QCmd); @@ -130,10 +132,7 @@ public void execute(int value, String sn) { * Sends slats to Qbus. */ public void executeSlats(int value, String sn) { - logger.info("Execute slats for {}", this.id); - - QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeStore").withId(this.id).withSlatState(value); - + QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeSlats").withId(this.id).withSlatState(value); QbusCommunication comm = QComm; if (comm != null) { comm.sendMessage(QCmd); diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java index adca51362c772..2a4b3db682e1a 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java @@ -15,8 +15,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusSceneHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusScene} class represents the action Qbus Scene output. @@ -27,32 +25,21 @@ @NonNullByDefault public final class QbusScene { - private final Logger logger = LoggerFactory.getLogger(QbusScene.class); - @Nullable private QbusCommunication QComm; - private String id = ""; - private Integer state = 0; + @Nullable + public QbusSceneHandler thingHandler; @Nullable - private QbusSceneHandler thingHandler; + private Integer state; + + private String id; QbusScene(String id) { this.id = id; } - /** - * This method should be called if the ThingHandler for the thing corresponding to this scene is initialized. - * It keeps a record of the thing handler in this object so the thing can be updated when - * the scene receives an update from the Qbus IP-interface. - * - * @param handler - */ - public void setThingHandler(QbusSceneHandler handler) { - this.thingHandler = handler; - } - /** * This method sets a pointer to the QComm Scene of class {@link QbusCommuncation}. * This is then used to be able to call back the sendCommand method in this class to send a command to the @@ -64,40 +51,31 @@ public void setQComm(QbusCommunication QComm) { this.QComm = QComm; } - /** - * Get state of scene. - * - * @return scene state - */ - public Integer getState() { - return this.state; - } - - /** - * Sets state of Scene. - * - * @param scene state - */ - void setState(int state) { - this.state = state; - QbusSceneHandler handler = thingHandler; - if (handler != null) { - logger.info("Update channel state for {} with {}", id, state); - handler.handleStateUpdate(this); - } - } - /** * Sends action to Qbus. */ public void execute(int value, String sn) { - logger.info("Execute scene for {} ", this.id); - QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeScene").withId(this.id).withState(value); - QbusCommunication comm = QComm; if (comm != null) { comm.sendMessage(QCmd); } } + + public void setThingHandler(QbusSceneHandler handler) { + this.thingHandler = handler; + } + + /** + * Get state of bistabiel. + * + * @return bistabiel state + */ + public @Nullable Integer getState() { + if (this.state != null) { + return this.state; + } else { + return null; + } + } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java index 294afba6f42bb..661f0705e9503 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java @@ -15,8 +15,6 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusThermostatHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusThermostat} class represents the thermostat Qbus communication object. It contains all @@ -29,8 +27,6 @@ @NonNullByDefault public final class QbusThermostat { - private final Logger logger = LoggerFactory.getLogger(QbusThermostat.class); - @Nullable private QbusCommunication qComm; @@ -60,7 +56,6 @@ public void updateState(Double measured, Double setpoint, Integer mode) { QbusThermostatHandler handler = thingHandler; if (handler != null) { - logger.info("Update channels for {}", id); handler.handleStateUpdate(this); } } @@ -148,10 +143,7 @@ private void setMode(Integer mode) { * @param sn */ public void executeMode(int mode, String sn) { - logger.info("Execute thermostat mode {} for {}", mode, id); - QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeThermostat").withId(this.id).withMode(mode); - QbusCommunication comm = qComm; if (comm != null) { comm.sendMessage(qCmd); @@ -164,10 +156,7 @@ public void executeMode(int mode, String sn) { * @param d */ public void executeSetpoint(double d, String sn) { - logger.info("Execute thermostat setpoint {} for {} ", d, id); - QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeThermostat").withId(this.id).withSetPoint(setpoint); - QbusCommunication comm = qComm; if (comm != null) { comm.sendMessage(qCmd); diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml index a7aef153dc7e5..3311fe5866eb4 100644 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml @@ -119,6 +119,25 @@ + + + + + Qbus Shutter control + + + + + + + + Qbus rol Id + false + + + + + From 96d8d832f392965564c7f72dabb9f00f0685afca Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Tue, 23 Feb 2021 09:50:28 +0100 Subject: [PATCH 10/17] Updated requested changes Updated requested changes by fwolter on 10/02/2021 Signed-off-by: Koen Schockaert Signed-off-by: QbusKoen --- bundles/org.openhab.binding.qbus/README.md | 3 +- .../qbus/internal/QbusBridgeHandler.java | 38 +++-- .../handler/QbusBistabielHandler.java | 27 ++-- .../qbus/internal/handler/QbusCO2Handler.java | 21 ++- .../internal/handler/QbusDimmerHandler.java | 30 ++-- .../qbus/internal/handler/QbusRolHandler.java | 45 +++--- .../internal/handler/QbusSceneHandler.java | 28 ++-- .../handler/QbusThermostatHandler.java | 33 ++--- .../qbus/internal/protocol/QbusBistabiel.java | 21 ++- .../qbus/internal/protocol/QbusCO2.java | 6 +- .../internal/protocol/QbusCommunication.java | 139 ++++++++---------- .../qbus/internal/protocol/QbusDimmer.java | 19 ++- .../protocol/QbusMessageDeserializer.java | 3 +- .../qbus/internal/protocol/QbusRol.java | 29 ++-- .../qbus/internal/protocol/QbusScene.java | 19 ++- .../internal/protocol/QbusThermostat.java | 26 +++- .../main/resources/OH-INF/binding/binding.xml | 4 +- .../resources/OH-INF/i18n/qbus_nl.properties | 99 +++++++++++++ .../OH-INF/i18n/qbus_xx_XX.properties | 17 --- .../resources/OH-INF/thing/thing-types.xml | 92 +++++------- 20 files changed, 370 insertions(+), 329 deletions(-) create mode 100644 bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties delete mode 100644 bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties diff --git a/bundles/org.openhab.binding.qbus/README.md b/bundles/org.openhab.binding.qbus/README.md index 3565e02b4b36b..a127b23876146 100644 --- a/bundles/org.openhab.binding.qbus/README.md +++ b/bundles/org.openhab.binding.qbus/README.md @@ -106,4 +106,5 @@ Rollershutter Roller2 {channel Dimmer Roller2_slats {channel="qbus:rollershutter_slats:CTD007841:121:slats"} ``` -This is the link to the [Qbus forum](https://qbusforum.be). This forum is mainly in dutch and you can find a lot of information about the pre testings af this binding and offers a way to communicate with other users. \ No newline at end of file +This is the link to the [Qbus forum](https://qbusforum.be). This forum is mainly in dutch and you can find a lot of information about the pre testings of this binding and offers a way to communicate with other users. + diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java index 2de20eb45c95c..16b2a6acc2eef 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java @@ -13,6 +13,7 @@ package org.openhab.binding.qbus.internal; +import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.concurrent.ScheduledFuture; @@ -27,6 +28,8 @@ import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseBridgeHandler; import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * {@link QbusBridgeHandler} is the handler for a Qbus controller @@ -37,9 +40,11 @@ @NonNullByDefault public class QbusBridgeHandler extends BaseBridgeHandler { + private final Logger logger = LoggerFactory.getLogger(QbusBridgeHandler.class); + private @Nullable QbusCommunication qbusComm; - protected @NonNullByDefault({}) QbusConfiguration config; + protected @Nullable QbusConfiguration config; private @Nullable ScheduledFuture refreshTimer; @@ -53,9 +58,10 @@ public QbusBridgeHandler(Bridge Bridge) { @Override public void initialize() { readConfig(); + InetAddress addr; Integer port = getPort(); - Integer Refresh = getRefresh(); + Integer refresh = getRefresh(); if (port == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, @@ -77,8 +83,8 @@ public void initialize() { "Incorrect ip address set for Qbus Server"); } - if (Refresh != null) { - this.setupRefreshTimer(Refresh); + if (refresh != null) { + this.setupRefreshTimer(refresh); } } @@ -105,7 +111,11 @@ private void createCommunicationObject(InetAddress addr, int port) { setBridgeCallBack(); if (qbusComm != null) { - qbusComm.startCommunication(); + try { + qbusComm.startCommunication(); + } catch (InterruptedException | IOException e) { + logger.warn("Error on restaring communication."); + } } if (qbusComm != null) { @@ -214,7 +224,7 @@ public void dispose() { * Sets the configuration parameters */ protected void readConfig() { - config = getConfig().as(QbusConfiguration.class); + this.config = getConfig().as(QbusConfiguration.class); } /** @@ -232,8 +242,8 @@ protected void readConfig() { * @return the ip address */ public @Nullable String getAddress() { - if (config != null) { - return config.addr; + if (this.config != null) { + return this.config.addr; } else { return null; } @@ -245,8 +255,8 @@ protected void readConfig() { * @return */ public @Nullable Integer getPort() { - if (config != null) { - return config.port; + if (this.config != null) { + return this.config.port; } else { return null; } @@ -258,8 +268,8 @@ protected void readConfig() { * @return the serial nr of the controller */ public @Nullable String getSn() { - if (config != null) { - return config.sn; + if (this.config != null) { + return this.config.sn; } else { return null; } @@ -271,8 +281,8 @@ protected void readConfig() { * @return the refresh interval */ public @Nullable Integer getRefresh() { - if (config != null) { - return config.refresh; + if (this.config != null) { + return this.config.refresh; } else { return null; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java index cd72eb6f75e30..a34fc179ecb82 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java @@ -38,23 +38,21 @@ @NonNullByDefault public class QbusBistabielHandler extends QbusGlobalHandler { - public QbusBistabielHandler(Thing thing) { - super(thing); - } + protected @Nullable QbusThingsConfig config; - protected @NonNullByDefault({}) QbusThingsConfig config; + private int bistabielId; - int bistabielId; + private @Nullable String sn; - @Nullable - private String sn; + public QbusBistabielHandler(Thing thing) { + super(thing); + } /** * Main initialization */ @Override public void initialize() { - readConfig(); bistabielId = getId(); @@ -133,7 +131,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (QBistabiel == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for bistabiel " + "bridge communication not initialized when trying to execute command for bistabiel " + bistabielId); return; } else { @@ -143,7 +141,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { } if (QComm.communicationActive()) { - if (command == REFRESH) { handleStateUpdate(QBistabiel); return; @@ -151,7 +148,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { handleSwitchCommand(QBistabiel, channelUID, command); } - }); } } else { @@ -162,11 +158,12 @@ public void handleCommand(ChannelUID channelUID, Command command) { /** * Executes the switch command + * + * @throws InterruptedException */ private void handleSwitchCommand(QbusBistabiel QBistabiel, ChannelUID channelUID, Command command) { if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; - @Nullable String snr = getSN(); if (snr != null) { if (s == OnOffType.OFF) { @@ -196,7 +193,7 @@ public void handleStateUpdate(QbusBistabiel QBistabiel) { * Read the configuration */ protected synchronized void readConfig() { - config = getConfig().as(QbusThingsConfig.class); + this.config = getConfig().as(QbusThingsConfig.class); } /** @@ -205,8 +202,8 @@ protected synchronized void readConfig() { * @return */ public int getId() { - if (config != null) { - return config.bistabielId; + if (this.config != null) { + return this.config.bistabielId; } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java index 500953834205e..804b6b9608409 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java @@ -39,16 +39,15 @@ @NonNullByDefault public class QbusCO2Handler extends QbusGlobalHandler { - public QbusCO2Handler(Thing thing) { - super(thing); - } + protected @Nullable QbusThingsConfig config; - protected @NonNullByDefault({}) QbusThingsConfig config; + private int co2Id; - int co2Id = 0; + private @Nullable String sn; - @Nullable - String sn; + public QbusCO2Handler(Thing thing) { + super(thing); + } /** * Main initialization @@ -111,7 +110,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { "Bridge communication not initialized when trying to execute command for CO2 " + co2Id); return; } else { - scheduler.submit(() -> { if (!QComm.communicationActive()) { restartCommunication(QComm, "CO2", co2Id); @@ -122,7 +120,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { handleStateUpdate(QCo2); return; } - } }); } @@ -167,7 +164,7 @@ public void setSN() { * Read the configuration */ protected synchronized void setConfig() { - config = getConfig().as(QbusThingsConfig.class); + this.config = getConfig().as(QbusThingsConfig.class); } /** @@ -176,8 +173,8 @@ protected synchronized void setConfig() { * @return co2Id */ public int getId() { - if (config != null) { - return config.co2Id; + if (this.config != null) { + return this.config.co2Id; } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java index a5d2193320bcc..c2d5281965be4 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java @@ -40,16 +40,15 @@ @NonNullByDefault public class QbusDimmerHandler extends QbusGlobalHandler { - public QbusDimmerHandler(Thing thing) { - super(thing); - } + protected @Nullable QbusThingsConfig config; - protected @NonNullByDefault({}) QbusThingsConfig config; + private int dimmerId; - int dimmerId; + private @Nullable String sn; - @Nullable - private String sn; + public QbusDimmerHandler(Thing thing) { + super(thing); + } /** * Main initialization @@ -134,14 +133,12 @@ public void handleCommand(ChannelUID channelUID, Command command) { "Bridge communication not initialized when trying to execute command for dimmer " + dimmerId); return; } else { - scheduler.submit(() -> { if (!QComm.communicationActive()) { restartCommunication(QComm, "Dimmer", dimmerId); } if (QComm.communicationActive()) { - if (command == REFRESH) { handleStateUpdate(QDimmer); return; @@ -150,17 +147,11 @@ public void handleCommand(ChannelUID channelUID, Command command) { switch (channelUID.getId()) { case CHANNEL_SWITCH: handleSwitchCommand(QDimmer, command); - updateStatus(ThingStatus.ONLINE); break; case CHANNEL_BRIGHTNESS: handleBrightnessCommand(QDimmer, command); - updateStatus(ThingStatus.ONLINE); break; - - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Channel unknown " + channelUID.getId()); } } }); @@ -185,7 +176,6 @@ public void thingOffline(String message) { private void handleSwitchCommand(QbusDimmer QDimmer, Command command) { if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; - @Nullable String snr = getSN(); if (s == OnOffType.OFF) { @@ -208,7 +198,6 @@ private void handleSwitchCommand(QbusDimmer QDimmer, Command command) { * Executes the brightness command */ private void handleBrightnessCommand(QbusDimmer QDimmer, Command command) { - @Nullable String snr = getSN(); if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; @@ -277,7 +266,6 @@ private void handleBrightnessCommand(QbusDimmer QDimmer, Command command) { * Method to update state of channel, called from Qbus Dimmer. */ public void handleStateUpdate(QbusDimmer QDimmer) { - Integer dimmerState = QDimmer.getState(); if (dimmerState != null) { updateState(CHANNEL_BRIGHTNESS, new PercentType(dimmerState)); @@ -289,7 +277,7 @@ public void handleStateUpdate(QbusDimmer QDimmer) { * Read the configuration */ protected synchronized void setConfig() { - config = getConfig().as(QbusThingsConfig.class); + this.config = getConfig().as(QbusThingsConfig.class); } /** @@ -298,8 +286,8 @@ protected synchronized void setConfig() { * @return dimmerId */ public int getId() { - if (config != null) { - return config.dimmerId; + if (this.config != null) { + return this.config.dimmerId; } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java index 92baabd4368e7..530e4dc2772a4 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java @@ -13,6 +13,7 @@ package org.openhab.binding.qbus.internal.handler; import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.library.types.UpDownType.DOWN; import static org.openhab.core.types.RefreshType.REFRESH; import java.util.Map; @@ -24,6 +25,7 @@ import org.openhab.binding.qbus.internal.protocol.QbusRol; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.UpDownType; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; @@ -40,16 +42,15 @@ @NonNullByDefault public class QbusRolHandler extends QbusGlobalHandler { - public QbusRolHandler(Thing thing) { - super(thing); - } + protected @Nullable QbusThingsConfig config; - protected @NonNullByDefault({}) QbusThingsConfig config; + private int rolId; - int rolId; + private @Nullable String sn; - @Nullable - String sn; + public QbusRolHandler(Thing thing) { + super(thing); + } /** * Main initialization @@ -142,7 +143,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { } if (QComm.communicationActive()) { - if (command == REFRESH) { handleStateUpdate(QRol); return; @@ -151,17 +151,11 @@ public void handleCommand(ChannelUID channelUID, Command command) { switch (channelUID.getId()) { case CHANNEL_ROLLERSHUTTER: handleScreenposCommand(QRol, command); - updateStatus(ThingStatus.ONLINE); break; case CHANNEL_SLATS: handleSlatsposCommand(QRol, command); - updateStatus(ThingStatus.ONLINE); break; - - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Channel unknown " + channelUID.getId()); } } }); @@ -181,11 +175,10 @@ public void thingOffline(String message) { * Executes the command for screen up/down position */ private void handleScreenposCommand(QbusRol QRol, Command command) { - @Nullable String snr = getSN(); - if (command instanceof org.openhab.core.library.types.UpDownType) { - org.openhab.core.library.types.UpDownType s = (org.openhab.core.library.types.UpDownType) command; - if (s == org.openhab.core.library.types.UpDownType.DOWN) { + if (command instanceof UpDownType) { + UpDownType upDown = (UpDownType) command; + if (upDown == DOWN) { if (snr != null) { QRol.execute(0, snr); } else { @@ -198,16 +191,14 @@ private void handleScreenposCommand(QbusRol QRol, Command command) { thingOffline("No serial number configured for " + rolId); } } - } else if (command instanceof IncreaseDecreaseType) - - { - IncreaseDecreaseType s = (IncreaseDecreaseType) command; + } else if (command instanceof IncreaseDecreaseType) { + IncreaseDecreaseType inc = (IncreaseDecreaseType) command; int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); Integer currentValue = QRol.getState(); int newValue; int sendValue; if (currentValue != null) { - if (s == IncreaseDecreaseType.INCREASE) { + if (inc == IncreaseDecreaseType.INCREASE) { newValue = currentValue + stepValue; // round down to step multiple newValue = newValue - newValue % stepValue; @@ -252,7 +243,6 @@ private void handleScreenposCommand(QbusRol QRol, Command command) { * Executes the command for screen slats position */ private void handleSlatsposCommand(QbusRol QRol, Command command) { - @Nullable String snr = getSN(); if (command instanceof org.openhab.core.library.types.UpDownType) { org.openhab.core.library.types.UpDownType s = (org.openhab.core.library.types.UpDownType) command; @@ -321,7 +311,6 @@ private void handleSlatsposCommand(QbusRol QRol, Command command) { * Method to update state of channel, called from Qbus Screen/Store. */ public void handleStateUpdate(QbusRol qRol) { - Integer rolState = qRol.getState(); Integer slatState = qRol.getStateSlats(); @@ -339,7 +328,7 @@ public void handleStateUpdate(QbusRol qRol) { * Read the configuration */ protected synchronized void setConfig() { - config = getConfig().as(QbusThingsConfig.class); + this.config = getConfig().as(QbusThingsConfig.class); } /** @@ -348,8 +337,8 @@ protected synchronized void setConfig() { * @return rolId */ public int getId() { - if (config != null) { - return config.rolId; + if (this.config != null) { + return this.config.rolId; } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java index 969a425e0707a..71d3973b708de 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java @@ -38,16 +38,15 @@ @NonNullByDefault public class QbusSceneHandler extends QbusGlobalHandler { - public QbusSceneHandler(Thing thing) { - super(thing); - } + protected @Nullable QbusThingsConfig config; - protected @NonNullByDefault({}) QbusThingsConfig config; + private int sceneId; - int sceneId; + private @Nullable String sn; - @Nullable - String sn; + public QbusSceneHandler(Thing thing) { + super(thing); + } /** * Main initialization @@ -134,23 +133,16 @@ public void handleCommand(ChannelUID channelUID, Command command) { "Bridge communication not initialized when trying to execute command for Scene " + sceneId); return; } else { - scheduler.submit(() -> { if (!QComm.communicationActive()) { restartCommunication(QComm, "Scene", sceneId); } if (QComm.communicationActive()) { - switch (channelUID.getId()) { case CHANNEL_SCENE: handleSwitchCommand(QScene, channelUID, command); - updateStatus(ThingStatus.ONLINE); break; - - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Channel unknown " + channelUID.getId()); } } }); @@ -162,7 +154,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { * Method to update state of channel, called from Qbus Scene. */ public void handleStateUpdate(QbusScene QScene) { - Integer sceneState = QScene.getState(); if (sceneState != null) { updateState(CHANNEL_SCENE, (sceneState == 0) ? OnOffType.OFF : OnOffType.ON); @@ -182,7 +173,6 @@ public void thingOffline(String message) { * Executes the scene command */ private void handleSwitchCommand(QbusScene QScene, ChannelUID channelUID, Command command) { - @Nullable String snr = getSN(); if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; @@ -206,7 +196,7 @@ private void handleSwitchCommand(QbusScene QScene, ChannelUID channelUID, Comman * Read the configuration */ protected synchronized void setConfig() { - config = getConfig().as(QbusThingsConfig.class); + this.config = getConfig().as(QbusThingsConfig.class); } /** @@ -215,8 +205,8 @@ protected synchronized void setConfig() { * @return sceneId */ public int getId() { - if (config != null) { - return config.sceneId; + if (this.config != null) { + return this.config.sceneId; } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java index 2b05ec07f59bb..1797f2a8c0fb9 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java @@ -41,16 +41,15 @@ @NonNullByDefault public class QbusThermostatHandler extends QbusGlobalHandler { - public QbusThermostatHandler(Thing thing) { - super(thing); - } + protected @Nullable QbusThingsConfig config; - protected @NonNullByDefault({}) QbusThingsConfig config; + private int thermostatId; - int thermostatId; + private @Nullable String sn; - @Nullable - String sn; + public QbusThermostatHandler(Thing thing) { + super(thing); + } /** * Main initialization @@ -140,37 +139,26 @@ public void handleCommand(ChannelUID channelUID, Command command) { + thermostatId); return; } else { - scheduler.submit(() -> { if (!QComm.communicationActive()) { restartCommunication(QComm, "Thermostat", thermostatId); } if (QComm.communicationActive()) { - if (command == REFRESH) { handleStateUpdate(QThermostat); return; } switch (channelUID.getId()) { - case CHANNEL_MEASURED: - updateStatus(ThingStatus.ONLINE); - break; - case CHANNEL_MODE: handleModeCommand(QThermostat, command); - updateStatus(ThingStatus.ONLINE); break; case CHANNEL_SETPOINT: handleSetpointCommand(QThermostat, command); - updateStatus(ThingStatus.ONLINE); break; - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Channel unknown " + channelUID.getId()); } } }); @@ -191,7 +179,6 @@ public void thingOffline(String message) { * Executes the Mode command */ private void handleModeCommand(QbusThermostat QThermostat, Command command) { - @Nullable String snr = getSN(); if (command instanceof DecimalType) { int mode = ((DecimalType) command).intValue(); @@ -207,7 +194,6 @@ private void handleModeCommand(QbusThermostat QThermostat, Command command) { * Executes the Setpoint command */ private void handleSetpointCommand(QbusThermostat QThermostat, Command command) { - @Nullable String snr = getSN(); if (command instanceof QuantityType) { QuantityType s = (QuantityType) command; @@ -227,7 +213,6 @@ private void handleSetpointCommand(QbusThermostat QThermostat, Command command) * */ public void handleStateUpdate(QbusThermostat QThermostat) { - updateState(CHANNEL_MEASURED, new QuantityType<>(QThermostat.getMeasured(), CELSIUS)); updateState(CHANNEL_SETPOINT, new QuantityType<>(QThermostat.getSetpoint(), CELSIUS)); @@ -241,7 +226,7 @@ public void handleStateUpdate(QbusThermostat QThermostat) { * Read the configuration */ protected synchronized void setConfig() { - config = getConfig().as(QbusThingsConfig.class); + this.config = getConfig().as(QbusThingsConfig.class); } /** @@ -250,8 +235,8 @@ protected synchronized void setConfig() { * @return dimmerId */ public int getId() { - if (config != null) { - return config.thermostatId; + if (this.config != null) { + return this.config.thermostatId; } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java index 7299c1da1dd4e..4e2b3b9ba70f5 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java @@ -15,6 +15,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusBistabielHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link QbusBistabiel} class represents the Qbus BISTABIEL output. @@ -25,16 +27,15 @@ @NonNullByDefault public final class QbusBistabiel { - @Nullable - private QbusCommunication QComm; + private final Logger logger = LoggerFactory.getLogger(QbusBistabiel.class); + + private @Nullable QbusCommunication QComm; private String id; - @Nullable - private Integer state; + private @Nullable Integer state; - @Nullable - private QbusBistabielHandler thingHandler; + private @Nullable QbusBistabielHandler thingHandler; QbusBistabiel(String id) { this.id = id; @@ -90,12 +91,18 @@ void setState(int state) { /** * Sends bistabiel to Qbus. + * + * @throws InterruptedException */ public void execute(int value, String sn) { QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeBistabiel").withId(this.id).withState(value); QbusCommunication comm = QComm; if (comm != null) { - comm.sendMessage(QCmd); + try { + comm.sendMessage(QCmd); + } catch (InterruptedException e) { + logger.warn("Could not send command for bistabiel {}", this.id); + } } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java index c8db92d174104..10c9a20f185c0 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java @@ -26,11 +26,9 @@ @NonNullByDefault public final class QbusCO2 { - @Nullable - private Integer state; + private @Nullable Integer state; - @Nullable - private QbusCO2Handler thingHandler; + private @Nullable QbusCO2Handler thingHandler; /** * This method should be called if the ThingHandler for the thing corresponding to this CO2 is initialized. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java index a042f8f20fea1..be90cca920adb 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java @@ -53,22 +53,19 @@ public final class QbusCommunication { private final Logger logger = LoggerFactory.getLogger(QbusCommunication.class); - @Nullable - private Socket qSocket; - @Nullable - private PrintWriter qOut; - @Nullable - private BufferedReader qIn; + private @Nullable Socket qSocket; + private @Nullable PrintWriter qOut; + private @Nullable BufferedReader qIn; private boolean listenerStopped; private boolean qEventsRunning; private Gson gsonOut = new Gson(); private Gson gsonIn; - @Nullable - private String CTD; - private Boolean CTDConnected = false; + private @Nullable String CTD; + + private boolean CTDConnected = false; private final Map bistabiel = new HashMap<>(); private final Map scene = new HashMap<>(); @@ -77,8 +74,7 @@ public final class QbusCommunication { private final Map thermostat = new HashMap<>(); private final Map co2 = new HashMap<>(); - @Nullable - private QbusBridgeHandler bridgeCallBack; + private @Nullable QbusBridgeHandler bridgeCallBack; /** * Constructor for Qbus communication object, manages communication with @@ -97,62 +93,53 @@ public QbusCommunication() { * * @param addr : IP-address of Qbus Server * @param port : Communication port of Qbus server + * @throws InterruptedException + * @throws IOException * */ - public synchronized void startCommunication() { + public synchronized void startCommunication() throws IOException, InterruptedException { QbusBridgeHandler handler = bridgeCallBack; CTDConnected = false; - try { - for (int i = 1; qEventsRunning && (i <= 5); i++) { - Thread.sleep(1000); - } - if (qEventsRunning) { - logger.error("Starting from thread {}, but previous connection still active after 5000ms", - Thread.currentThread().getId()); - throw new IOException(); - } - - if (handler == null) { - throw new IOException(); - } + if (qEventsRunning) { + throw new IOException("Previous listening thread is still active."); + } - InetAddress addr = InetAddress.getByName(handler.getAddress()); - Integer port = handler.getPort(); + if (handler == null) { + throw new IOException("No Bridge handler initialised."); + } - if (port != null) { - Socket socket = new Socket(addr, port); - qSocket = socket; - qOut = new PrintWriter(socket.getOutputStream(), true); - qIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); - logger.info("Connected via local port {} from thread {}", socket.getLocalPort(), - Thread.currentThread().getId()); - } else { - return; - } + InetAddress addr = InetAddress.getByName(handler.getAddress()); + Integer port = handler.getPort(); - setSN(); - getSN(); + if (port != null) { + Socket socket = new Socket(addr, port); + qSocket = socket; + qOut = new PrintWriter(socket.getOutputStream(), true); + qIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); + logger.debug("Connected via local port {} from thread {}", socket.getLocalPort(), + Thread.currentThread().getId()); + } else { + return; + } - // Connect to Qbus server - Connect(); + setSN(); + getSN(); - // If Qbus Client is connected then initialize, else put Bridge offline - if (CTDConnected == true) { - initialize(); - (new Thread(qEvents)).start(); - } else { - handler.bridgeOffline("No communication with Qbus client"); - } - } catch (IOException | InterruptedException e) { - logger.warn("Error initializing communication from thread {}", Thread.currentThread().getId()); - // No connection with Qbus server, put Bridge offline - stopCommunication(); - if (handler != null) { - handler.bridgeOffline("No communication with Qbus server"); - } + // Connect to Qbus server + Connect(); + // If Qbus Client is connected then initialize and start listening for incoming commands, else put Bridge + // offline + if (CTDConnected == true) { + initialize(); + Thread thread = new Thread(this::qEvents); + thread.setName("OH-binding"); + thread.setDaemon(true); + thread.start(); + } else { + handler.bridgeOffline("No communication with Qbus client"); } } @@ -183,13 +170,19 @@ public synchronized void stopCommunication() { /** * Close and restart communication with Qbus Server. + * + * @throws InterruptedException */ public synchronized void restartCommunication() { stopCommunication(); logger.debug("Qbus: restart communication from thread {}", Thread.currentThread().getId()); - startCommunication(); + try { + startCommunication(); + } catch (InterruptedException | IOException e) { + logger.warn("Error on restaring communication."); + } } /** @@ -198,7 +191,6 @@ public synchronized void restartCommunication() { * @return True if active */ public boolean communicationActive() { - return qSocket != null; } @@ -219,8 +211,10 @@ public boolean clientConnected() { * and interprets all incomming json messages. It triggers state updates for active channels linked to the * Qbus outputs. It is started after initialization of the communication. * + * @return + * */ - private Runnable qEvents = () -> { + private void qEvents() { String qMessage; logger.info("Listening for events on thread {}", Thread.currentThread().getId()); @@ -231,9 +225,9 @@ public boolean clientConnected() { try { if (reader == null) { - throw new IOException(); + throw new IOException("Bufferreader for incomming messages not initialized."); } - while (!listenerStopped & ((qMessage = reader.readLine()) != null)) { + while (!Thread.currentThread().isInterrupted() & ((qMessage = reader.readLine()) != null)) { if (qMessage != null) { readMessage(qMessage); } @@ -241,7 +235,7 @@ public boolean clientConnected() { } catch (IOException e) { if (!listenerStopped) { qEventsRunning = false; - logger.warn("Qbus: IO error in listener on thread {}", Thread.currentThread().getId()); + logger.error("Qbus: IO error in listener on thread {}", Thread.currentThread().getId()); QbusBridgeHandler handler = bridgeCallBack; @@ -249,23 +243,20 @@ public boolean clientConnected() { CTDConnected = false; handler.bridgeOffline("No communication with Qbus server"); } - - return; } } qEventsRunning = false; - - logger.warn("Event listener thread stopped on thread {}", Thread.currentThread().getId()); - + logger.trace("Event listener thread stopped on thread {}", Thread.currentThread().getId()); }; /** * Called by other methods to send json data to Qbus. * * @param qMessage + * @throws InterruptedException */ - synchronized void sendMessage(Object qMessage) { + synchronized void sendMessage(Object qMessage) throws InterruptedException { PrintWriter writer = qOut; String json = gsonOut.toJson(qMessage); @@ -275,7 +266,7 @@ synchronized void sendMessage(Object qMessage) { try { TimeUnit.MILLISECONDS.sleep(250); } catch (InterruptedException e) { - // No reaction on error is required + throw new InterruptedException(e.toString()); } } @@ -301,7 +292,6 @@ private void sendAndReadMessage(String command) throws IOException, InterruptedE @Nullable String snr = getSN(); if (snr != null) { - QbusMessageCmd qCmd = new QbusMessageCmd(snr, command); sendMessage(qCmd); @@ -342,7 +332,6 @@ private void readMessage(String qMessage) { if (sn != null && CTD != null) { try { - if (Integer.parseInt(sn) == Integer.parseInt(CTD) && qMessageGson != null) { // Get the compatible outputs from the Qbus server if ("returnBistabiel".equals(cmd)) { @@ -352,7 +341,7 @@ private void readMessage(String qMessage) { } else if (("returnThermostat").equals(cmd)) { cmdListThermostat(((QbusMessageListMap) qMessageGson).getOutputs()); } else if (("returnScene").equals(cmd)) { - cmdlistscenes(((QbusMessageListMap) qMessageGson).getOutputs()); + cmdListscenes(((QbusMessageListMap) qMessageGson).getOutputs()); } else if (("returnCo2").equals(cmd)) { cmdlistco2(((QbusMessageListMap) qMessageGson).getOutputs()); } else if (("returnRol02p").equals(cmd)) { @@ -409,7 +398,6 @@ else if ("disconnect".equals(cmd)) { */ private void initialize() throws IOException, InterruptedException { - if (bridgeCallBack != null) { if (CTDConnected) { sendAndReadMessage("getBistabiel"); @@ -431,7 +419,7 @@ private void initialize() throws IOException, InterruptedException { return; } } else { - logger.error("Initialization error"); + logger.trace("Initialization error"); } } @@ -510,7 +498,7 @@ private void cmdListBistabiel(@Nullable List> outputs) { * * @param outputs */ - private void cmdlistscenes(@Nullable List> outputs) { + private void cmdListscenes(@Nullable List> outputs) { if (outputs != null) { for (Map scene : outputs) { String idStr = scene.get("id"); @@ -588,7 +576,6 @@ private void cmdlistrol(@Nullable List> outputs) { * @param data */ private void cmdlistrolslats(@Nullable List> outputs) { - logger.debug("Qbus: ROL02PSLATS received from Qbus server"); if (outputs != null) { for (Map rol : outputs) { String idStr = rol.get("id"); @@ -891,7 +878,7 @@ private void connection() { } /** - * Return all screen outûts in the Qbus Controller. + * Return all rollershutter outûts in the Qbus Controller. * * @return */ diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java index 60c0e5759f68a..6b539aeb60408 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java @@ -15,6 +15,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusDimmerHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link QbusDimmer} class represents the action Qbus Dimmer output. @@ -25,16 +27,15 @@ @NonNullByDefault public final class QbusDimmer { - @Nullable - private QbusCommunication QComm; + private final Logger logger = LoggerFactory.getLogger(QbusDimmer.class); + + private @Nullable QbusCommunication QComm; private String id; - @Nullable - private Integer state; + private @Nullable Integer state; - @Nullable - private QbusDimmerHandler thingHandler; + private @Nullable QbusDimmerHandler thingHandler; QbusDimmer(String id) { this.id = id; @@ -109,7 +110,11 @@ public void execute(int percent, String sn) { QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeDimmer").withId(this.id).withState(percent); QbusCommunication comm = QComm; if (comm != null) { - comm.sendMessage(QCmd); + try { + comm.sendMessage(QCmd); + } catch (InterruptedException e) { + logger.warn("Could not send command for dimmer {}", this.id); + } } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java index 17a9edc669126..41c3c013b761f 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java @@ -46,7 +46,6 @@ class QbusMessageDeserializer implements JsonDeserializer { final JsonObject jsonObject = json.getAsJsonObject(); try { - String cmd = null; String CTD = null; @@ -104,7 +103,7 @@ class QbusMessageDeserializer implements JsonDeserializer { return message; - } catch (IllegalStateException | ClassCastException e) { + } catch (IllegalStateException e) { throw new JsonParseException("Unexpected Json type"); } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java index 3b9389e999f22..b6190a23bb2e4 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java @@ -15,6 +15,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusRolHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link QbusRol} class represents the action Qbus Shutter/Slats output. @@ -25,18 +27,17 @@ @NonNullByDefault public final class QbusRol { - @Nullable - private QbusCommunication QComm; + private final Logger logger = LoggerFactory.getLogger(QbusRol.class); + + private @Nullable QbusCommunication QComm; private String id; - @Nullable - private Integer state; - @Nullable - private Integer slats; + private @Nullable Integer state; + + private @Nullable Integer slats; - @Nullable - private QbusRolHandler thingHandler; + private @Nullable QbusRolHandler thingHandler; QbusRol(String id) { this.id = id; @@ -124,7 +125,11 @@ public void execute(int value, String sn) { QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeStore").withId(this.id).withState(value); QbusCommunication comm = QComm; if (comm != null) { - comm.sendMessage(QCmd); + try { + comm.sendMessage(QCmd); + } catch (InterruptedException e) { + logger.warn("Could not send command for store {}", this.id); + } } } @@ -135,7 +140,11 @@ public void executeSlats(int value, String sn) { QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeSlats").withId(this.id).withSlatState(value); QbusCommunication comm = QComm; if (comm != null) { - comm.sendMessage(QCmd); + try { + comm.sendMessage(QCmd); + } catch (InterruptedException e) { + logger.warn("Could not send command for slat {}", this.id); + } } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java index 2a4b3db682e1a..1e7f8d38f26d7 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java @@ -15,6 +15,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusSceneHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link QbusScene} class represents the action Qbus Scene output. @@ -25,14 +27,13 @@ @NonNullByDefault public final class QbusScene { - @Nullable - private QbusCommunication QComm; + private final Logger logger = LoggerFactory.getLogger(QbusScene.class); - @Nullable - public QbusSceneHandler thingHandler; + private @Nullable QbusCommunication QComm; - @Nullable - private Integer state; + public @Nullable QbusSceneHandler thingHandler; + + private @Nullable Integer state; private String id; @@ -58,7 +59,11 @@ public void execute(int value, String sn) { QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeScene").withId(this.id).withState(value); QbusCommunication comm = QComm; if (comm != null) { - comm.sendMessage(QCmd); + try { + comm.sendMessage(QCmd); + } catch (InterruptedException e) { + logger.warn("Could not send command for scene {}", this.id); + } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java index 661f0705e9503..f874c2d3485bc 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java @@ -15,6 +15,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusThermostatHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link QbusThermostat} class represents the thermostat Qbus communication object. It contains all @@ -27,16 +29,16 @@ @NonNullByDefault public final class QbusThermostat { - @Nullable - private QbusCommunication qComm; + private final Logger logger = LoggerFactory.getLogger(QbusThermostat.class); + + private @Nullable QbusCommunication qComm; private String id; - private Double measured = 0.0; - private Double setpoint = 0.0; + private double measured = 0.0; + private double setpoint = 0.0; private Integer mode = 0; - @Nullable - private QbusThermostatHandler thingHandler; + private @Nullable QbusThermostatHandler thingHandler; QbusThermostat(String id) { this.id = id; @@ -146,7 +148,11 @@ public void executeMode(int mode, String sn) { QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeThermostat").withId(this.id).withMode(mode); QbusCommunication comm = qComm; if (comm != null) { - comm.sendMessage(qCmd); + try { + comm.sendMessage(qCmd); + } catch (InterruptedException e) { + logger.warn("Could not send command mode for thermostat {}", this.id); + } } } @@ -159,7 +165,11 @@ public void executeSetpoint(double d, String sn) { QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeThermostat").withId(this.id).withSetPoint(setpoint); QbusCommunication comm = qComm; if (comm != null) { - comm.sendMessage(qCmd); + try { + comm.sendMessage(qCmd); + } catch (InterruptedException e) { + logger.warn("Could not send command setpoitn for thermostat {}", this.id); + } } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml index b18daa90afc8d..2457b9d1b1a7d 100644 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml @@ -4,7 +4,7 @@ xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd"> Qbus Binding - This is the binding for Qbus. - Koen Schockaert + This is the binding for the Qbus home automation system. Qbus is a system made and developed in Belgium + (https://www.qbus.be/nl-nl) diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties new file mode 100644 index 0000000000000..bca15973e1883 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties @@ -0,0 +1,99 @@ +# FIXME: please substitute the xx_XX with a proper locale, ie. de_DE +# FIXME: please do not add the file to the repo if you add or change no content + +# binding +binding.qbus.name = Qbus Binding +binding.qbus.description = Deze binding maakt via een server applicate verbinding met de Qbus controller. + +# thing types +thing-type.qbus.bridge.label = Qbus Bridge +thing-type.qbus.bridge.description = De Qbus Bridge Maakt Verbinding Met de Qbus Server. +thing-type.config.qbus.bridge.addr.label = IP Adres of Host Naam +thing-type.config.qbus.bridge.addr.description = IP adres van de Qbus server, meestal 'localhost' +thing-type.config.qbus.bridge.sn.label = Serienummer van de Controller +thing-type.config.qbus.bridge.sn.description = Serienummer van de CTD controller +thing-type.config.qbus.bridge.port.label = Poort +thing-type.config.qbus.bridge.port.description = Communicatiepoort van de Qbus server (standaard: 8447) +thing-type.config.qbus.bridge.refresh.label = Refresh Timer +thing-type.config.qbus.bridge.refresh.description = Ingestelde timer, bij het verlopen van de timer zal de communicatie met Client/Server gecontroleerd worden en indien nodig herstart. + +thing-type.qbus.onOff.label = Aan/uit +thing-type.qbus.onOff.description = Alle Bistabiel-Mono-Timer-Interval uitgangen +thing-type.config.qbus.onOff.bistabielId.label = Qbus ID +thing-type.config.qbus.onOff.bistabielId.description = Identificatienummer van de uitgang (zie SMIII) + +thing-type.qbus.scene.label = Sfeer +thing-type.qbus.scene.description = Alle sferen +thing-type.config.qbus.scene.sceneId.label = Qbus ID +thing-type.config.qbus.scene.sceneId.description = Identificatienummer van de sfeer (zie SMIII) + +thing-type.qbus.co2.label = CO2 +thing-type.qbus.co2.description = Alle CO2 Uitgangen +thing-type.config.qbus.co2.co2Id.label = Qbus ID +thing-type.config.qbus.co2.co2Id.description = Identificatienummer van de uitgang (zie SMIII) + +thing-type.qbus.dimmer.label = Dimmer +thing-type.qbus.dimmer.description = Alle Dimbare Uitgangen +thing-type.config.qbus.dimmer.dimmerId.label = Qbus ID +thing-type.config.qbus.dimmer.dimmerId.description = Identificatienummer van de uitgang (zie SMIII) +thing-type.config.qbus.dimmer.step.label = Stappenwaarde +thing-type.config.qbus.dimmer.step.description = Waarde gebruikt voor het dimmen in stappen (standaard 10%) + +thing-type.qbus.rollershutter.label = Rolluik +thing-type.qbus.rollershutter.description = Alle Rolluik (ROL02P) Uitgangen +thing-type.config.qbus.rollershutter.rolId.label = Qbus ID +thing-type.config.qbus.rollershutter.rolId.description = Identificatienummer van de uitgang (zie SMIII) + +thing-type.qbus.rollershutter_slats.label = Rolluik (met lamellen) +thing-type.qbus.rollershutter_slats.description = Alle schermen met lamellen (ROL02P) uitgang +thing-type.config.qbus.rollershutter_slats.rolId.label = Qbus ID +thing-type.config.qbus.rollershutter_slats.rolId.description = Identificatienummer van de uitgang (zie SMIII) + +thing-type.qbus.thermostat.label = Thermostaat +thing-type.qbus.thermostat.description = Alle thermostaten +thing-type.config.qbus.thermostat.thermostatId.label = Qbus ID +thing-type.config.qbus.thermostat.thermostatId.description = Identificatienummer van de uitgang (zie SMIII) + +channel-type.qbus.scene.label = Sfeer +channel-type.qbus.scene.description = Bediening van de sfeer + +channel-type.qbus.co2.label = CO2 +channel-type.qbus.co2.description = Uitlezing van de CO2 waarde + +channel-type.qbus.switch.label = Schakelaar +channel-type.qbus.switch.description = Schakelaar bediening van de uitgangen + +channel-type.qbus.brightness.label = Helderheid +channel-type.qbus.brightness.description = Helderheid bediening van de uitgangen + +channel-type.qbus.measured.label = Gemeten Temperatuur +channel-type.qbus.measured.description = Uitlezing van de gemeten Temperatuur + +channel-type.qbus.setpoint.label = Ingestelde Temperatuur +channel-type.qbus.setpoint.description = Ingestelde temperatuur bediening van de uitgangen + +channel-type.qbus.mode.label = Ingesteld Regime +channel-type.qbus.mode.description = Regime bediening van de uitgangen +channel-type.qbus.mode.state.option.0 = Manueel +channel-type.qbus.mode.state.option.1 = Vorst +channel-type.qbus.mode.state.option.2 = Economisch +channel-type.qbus.mode.state.option.3 = Comfort +channel-type.qbus.mode.state.option.4 = Nacht + + +channel-type.qbus.rollershutter.label = Rolluik Bediening +channel-type.qbus.rollershutter.description = Rolluik bediening van de uitgangen + +channel-type.qbus.slats.label = Lamellen Bediening +channel-type.qbus.slats.description = Lamellen bediening van de uitgangen + + + + +# thing type config description +thing-type.config.qbus.sample.config1.label = +thing-type.config.qbus.sample.config1.description = + +# channel types +channel-type.qbus.sample-channel.label = +channel-type.qbus.sample-channel.description = diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties deleted file mode 100644 index 0c7193b3f16c7..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties +++ /dev/null @@ -1,17 +0,0 @@ -# FIXME: please substitute the xx_XX with a proper locale, ie. de_DE -# FIXME: please do not add the file to the repo if you add or change no content -# binding -binding.qbus.name = -binding.qbus.description = - -# thing types -thing-type.qbus.sample.label = -thing-type.qbus.sample.description = - -# thing type config description -thing-type.config.qbus.sample.config1.label = -thing-type.config.qbus.sample.config1.description = - -# channel types -channel-type.qbus.sample-channel.label = -channel-type.qbus.sample-channel.description = diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml index 3311fe5866eb4..fd68182c27f35 100644 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml @@ -9,29 +9,26 @@ This bridge represents a Qbus client - - IP Address of Qbus server, usually 'localhost' + + IP Address of Qbus Server, Usually 'localhost' localhost - false network-address - Serial nr of the CTD controller - - false + Serial Number of the CTD Controller - Port to communicate with Qbus server, default 8447 + Port To Communicate With Qbus Server, Default 8447 8447 true - Refresh interval for connection with Qbus server (min), default 300. If set to 0 or left empty, no - refresh will be scheduled - 300 + Refresh Interval for Connection With Qbus Server (min), default 10. If Set To 0 or Left Empty, No + Refresh Will Be Scheduled + 10 true @@ -42,15 +39,14 @@ - Bistabiel-Mono-Timer-Interval output + Bistabiel-Mono-Timer-Interval Output - + - Qbus IP Interface Output Object ID - false + Qbus Bistabiel ID @@ -60,15 +56,14 @@ - Qbus scene + Qbus Scene - - Qbus scene ID - false + + Qbus Scene ID @@ -87,7 +82,6 @@ Qbus CO2 ID - false @@ -97,19 +91,18 @@ - Qbus dimmer output + Qbus Dimmer Output - + - Qbus IP Interface Output ID - false + Qbus Dimmer ID - Step value used for increase/decrease of dimmer brightness, default 10% + Step Value Used for Increase/Decrease of Dimmer Brightness, Default 10% 10 true @@ -123,16 +116,14 @@ - Qbus Shutter control + Qbus Shutter (ROL02P) Control - - Qbus rol Id - false + Qbus Rol Id @@ -141,8 +132,8 @@ - - Qbus Shutter control + + Qbus Shutter With Slats Control @@ -150,8 +141,7 @@ - Qbus rol Id - false + Qbus Rol Id @@ -161,7 +151,7 @@ - Qbus thermostat + Qbus Thermostat @@ -170,8 +160,7 @@ - Qbus IP Interface Thermostat ID - false + Qbus Thermostat ID @@ -179,42 +168,35 @@ Switch - Scene control for Qbus + Scene Control for Qbus Scene Number - CO2 value for Qbus + CO2 Value for Qbus CO2 Switch - Switch control for output in Qbus + Switch Control for Output in Qbus Switch Dimmer - Brightness control for dimmer function in Qbus + Brightness Control for Dimmer Function in Qbus DimmableLight - - Color - - DMX control for dimmer function in Qbus - HSB - - Number:Temperature - Temperature measured by thermostat + Temperature Measured by Thermostat Temperature CurrentTemperature @@ -225,7 +207,7 @@ Number:Temperature - Setpoint temperature of thermostat + Setpoint Temperature of Thermostat Temperature TargetTemperature @@ -236,15 +218,15 @@ Number - Thermostat mode + Thermostat Mode Number - - - + + + - + @@ -252,14 +234,14 @@ Rollershutter - Rollershutter control for Qbus + Rollershutter Control for Qbus Blinds Dimmer - Slatcontrol function in Qbus + Slatcontrol Function in Qbus Blinds From 10bbea6583b9022d0c7b65fa5d9b6b8200f54068 Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Tue, 23 Feb 2021 09:50:28 +0100 Subject: [PATCH 11/17] Updated requested changes Updated requested changes by fwolter on 10/02/2021 Signed-off-by: Koen Schockaert Signed-off-by: QbusKoen --- bundles/org.openhab.binding.qbus/README.md | 3 +- .../qbus/internal/QbusBridgeHandler.java | 38 +++-- .../handler/QbusBistabielHandler.java | 27 ++-- .../qbus/internal/handler/QbusCO2Handler.java | 21 ++- .../internal/handler/QbusDimmerHandler.java | 30 ++-- .../qbus/internal/handler/QbusRolHandler.java | 45 +++--- .../internal/handler/QbusSceneHandler.java | 28 ++-- .../handler/QbusThermostatHandler.java | 33 ++--- .../qbus/internal/protocol/QbusBistabiel.java | 21 ++- .../qbus/internal/protocol/QbusCO2.java | 6 +- .../internal/protocol/QbusCommunication.java | 139 ++++++++---------- .../qbus/internal/protocol/QbusDimmer.java | 19 ++- .../protocol/QbusMessageDeserializer.java | 3 +- .../qbus/internal/protocol/QbusRol.java | 29 ++-- .../qbus/internal/protocol/QbusScene.java | 19 ++- .../internal/protocol/QbusThermostat.java | 26 +++- .../main/resources/OH-INF/binding/binding.xml | 4 +- .../resources/OH-INF/i18n/qbus_nl.properties | 99 +++++++++++++ .../OH-INF/i18n/qbus_xx_XX.properties | 17 --- .../resources/OH-INF/thing/thing-types.xml | 92 +++++------- 20 files changed, 370 insertions(+), 329 deletions(-) create mode 100644 bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties delete mode 100644 bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties diff --git a/bundles/org.openhab.binding.qbus/README.md b/bundles/org.openhab.binding.qbus/README.md index 3565e02b4b36b..a127b23876146 100644 --- a/bundles/org.openhab.binding.qbus/README.md +++ b/bundles/org.openhab.binding.qbus/README.md @@ -106,4 +106,5 @@ Rollershutter Roller2 {channel Dimmer Roller2_slats {channel="qbus:rollershutter_slats:CTD007841:121:slats"} ``` -This is the link to the [Qbus forum](https://qbusforum.be). This forum is mainly in dutch and you can find a lot of information about the pre testings af this binding and offers a way to communicate with other users. \ No newline at end of file +This is the link to the [Qbus forum](https://qbusforum.be). This forum is mainly in dutch and you can find a lot of information about the pre testings of this binding and offers a way to communicate with other users. + diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java index 2de20eb45c95c..16b2a6acc2eef 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java @@ -13,6 +13,7 @@ package org.openhab.binding.qbus.internal; +import java.io.IOException; import java.net.InetAddress; import java.net.UnknownHostException; import java.util.concurrent.ScheduledFuture; @@ -27,6 +28,8 @@ import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseBridgeHandler; import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * {@link QbusBridgeHandler} is the handler for a Qbus controller @@ -37,9 +40,11 @@ @NonNullByDefault public class QbusBridgeHandler extends BaseBridgeHandler { + private final Logger logger = LoggerFactory.getLogger(QbusBridgeHandler.class); + private @Nullable QbusCommunication qbusComm; - protected @NonNullByDefault({}) QbusConfiguration config; + protected @Nullable QbusConfiguration config; private @Nullable ScheduledFuture refreshTimer; @@ -53,9 +58,10 @@ public QbusBridgeHandler(Bridge Bridge) { @Override public void initialize() { readConfig(); + InetAddress addr; Integer port = getPort(); - Integer Refresh = getRefresh(); + Integer refresh = getRefresh(); if (port == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, @@ -77,8 +83,8 @@ public void initialize() { "Incorrect ip address set for Qbus Server"); } - if (Refresh != null) { - this.setupRefreshTimer(Refresh); + if (refresh != null) { + this.setupRefreshTimer(refresh); } } @@ -105,7 +111,11 @@ private void createCommunicationObject(InetAddress addr, int port) { setBridgeCallBack(); if (qbusComm != null) { - qbusComm.startCommunication(); + try { + qbusComm.startCommunication(); + } catch (InterruptedException | IOException e) { + logger.warn("Error on restaring communication."); + } } if (qbusComm != null) { @@ -214,7 +224,7 @@ public void dispose() { * Sets the configuration parameters */ protected void readConfig() { - config = getConfig().as(QbusConfiguration.class); + this.config = getConfig().as(QbusConfiguration.class); } /** @@ -232,8 +242,8 @@ protected void readConfig() { * @return the ip address */ public @Nullable String getAddress() { - if (config != null) { - return config.addr; + if (this.config != null) { + return this.config.addr; } else { return null; } @@ -245,8 +255,8 @@ protected void readConfig() { * @return */ public @Nullable Integer getPort() { - if (config != null) { - return config.port; + if (this.config != null) { + return this.config.port; } else { return null; } @@ -258,8 +268,8 @@ protected void readConfig() { * @return the serial nr of the controller */ public @Nullable String getSn() { - if (config != null) { - return config.sn; + if (this.config != null) { + return this.config.sn; } else { return null; } @@ -271,8 +281,8 @@ protected void readConfig() { * @return the refresh interval */ public @Nullable Integer getRefresh() { - if (config != null) { - return config.refresh; + if (this.config != null) { + return this.config.refresh; } else { return null; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java index cd72eb6f75e30..a34fc179ecb82 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java @@ -38,23 +38,21 @@ @NonNullByDefault public class QbusBistabielHandler extends QbusGlobalHandler { - public QbusBistabielHandler(Thing thing) { - super(thing); - } + protected @Nullable QbusThingsConfig config; - protected @NonNullByDefault({}) QbusThingsConfig config; + private int bistabielId; - int bistabielId; + private @Nullable String sn; - @Nullable - private String sn; + public QbusBistabielHandler(Thing thing) { + super(thing); + } /** * Main initialization */ @Override public void initialize() { - readConfig(); bistabielId = getId(); @@ -133,7 +131,7 @@ public void handleCommand(ChannelUID channelUID, Command command) { if (QBistabiel == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for bistabiel " + "bridge communication not initialized when trying to execute command for bistabiel " + bistabielId); return; } else { @@ -143,7 +141,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { } if (QComm.communicationActive()) { - if (command == REFRESH) { handleStateUpdate(QBistabiel); return; @@ -151,7 +148,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { handleSwitchCommand(QBistabiel, channelUID, command); } - }); } } else { @@ -162,11 +158,12 @@ public void handleCommand(ChannelUID channelUID, Command command) { /** * Executes the switch command + * + * @throws InterruptedException */ private void handleSwitchCommand(QbusBistabiel QBistabiel, ChannelUID channelUID, Command command) { if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; - @Nullable String snr = getSN(); if (snr != null) { if (s == OnOffType.OFF) { @@ -196,7 +193,7 @@ public void handleStateUpdate(QbusBistabiel QBistabiel) { * Read the configuration */ protected synchronized void readConfig() { - config = getConfig().as(QbusThingsConfig.class); + this.config = getConfig().as(QbusThingsConfig.class); } /** @@ -205,8 +202,8 @@ protected synchronized void readConfig() { * @return */ public int getId() { - if (config != null) { - return config.bistabielId; + if (this.config != null) { + return this.config.bistabielId; } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java index 500953834205e..804b6b9608409 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java @@ -39,16 +39,15 @@ @NonNullByDefault public class QbusCO2Handler extends QbusGlobalHandler { - public QbusCO2Handler(Thing thing) { - super(thing); - } + protected @Nullable QbusThingsConfig config; - protected @NonNullByDefault({}) QbusThingsConfig config; + private int co2Id; - int co2Id = 0; + private @Nullable String sn; - @Nullable - String sn; + public QbusCO2Handler(Thing thing) { + super(thing); + } /** * Main initialization @@ -111,7 +110,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { "Bridge communication not initialized when trying to execute command for CO2 " + co2Id); return; } else { - scheduler.submit(() -> { if (!QComm.communicationActive()) { restartCommunication(QComm, "CO2", co2Id); @@ -122,7 +120,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { handleStateUpdate(QCo2); return; } - } }); } @@ -167,7 +164,7 @@ public void setSN() { * Read the configuration */ protected synchronized void setConfig() { - config = getConfig().as(QbusThingsConfig.class); + this.config = getConfig().as(QbusThingsConfig.class); } /** @@ -176,8 +173,8 @@ protected synchronized void setConfig() { * @return co2Id */ public int getId() { - if (config != null) { - return config.co2Id; + if (this.config != null) { + return this.config.co2Id; } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java index a5d2193320bcc..c2d5281965be4 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java @@ -40,16 +40,15 @@ @NonNullByDefault public class QbusDimmerHandler extends QbusGlobalHandler { - public QbusDimmerHandler(Thing thing) { - super(thing); - } + protected @Nullable QbusThingsConfig config; - protected @NonNullByDefault({}) QbusThingsConfig config; + private int dimmerId; - int dimmerId; + private @Nullable String sn; - @Nullable - private String sn; + public QbusDimmerHandler(Thing thing) { + super(thing); + } /** * Main initialization @@ -134,14 +133,12 @@ public void handleCommand(ChannelUID channelUID, Command command) { "Bridge communication not initialized when trying to execute command for dimmer " + dimmerId); return; } else { - scheduler.submit(() -> { if (!QComm.communicationActive()) { restartCommunication(QComm, "Dimmer", dimmerId); } if (QComm.communicationActive()) { - if (command == REFRESH) { handleStateUpdate(QDimmer); return; @@ -150,17 +147,11 @@ public void handleCommand(ChannelUID channelUID, Command command) { switch (channelUID.getId()) { case CHANNEL_SWITCH: handleSwitchCommand(QDimmer, command); - updateStatus(ThingStatus.ONLINE); break; case CHANNEL_BRIGHTNESS: handleBrightnessCommand(QDimmer, command); - updateStatus(ThingStatus.ONLINE); break; - - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Channel unknown " + channelUID.getId()); } } }); @@ -185,7 +176,6 @@ public void thingOffline(String message) { private void handleSwitchCommand(QbusDimmer QDimmer, Command command) { if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; - @Nullable String snr = getSN(); if (s == OnOffType.OFF) { @@ -208,7 +198,6 @@ private void handleSwitchCommand(QbusDimmer QDimmer, Command command) { * Executes the brightness command */ private void handleBrightnessCommand(QbusDimmer QDimmer, Command command) { - @Nullable String snr = getSN(); if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; @@ -277,7 +266,6 @@ private void handleBrightnessCommand(QbusDimmer QDimmer, Command command) { * Method to update state of channel, called from Qbus Dimmer. */ public void handleStateUpdate(QbusDimmer QDimmer) { - Integer dimmerState = QDimmer.getState(); if (dimmerState != null) { updateState(CHANNEL_BRIGHTNESS, new PercentType(dimmerState)); @@ -289,7 +277,7 @@ public void handleStateUpdate(QbusDimmer QDimmer) { * Read the configuration */ protected synchronized void setConfig() { - config = getConfig().as(QbusThingsConfig.class); + this.config = getConfig().as(QbusThingsConfig.class); } /** @@ -298,8 +286,8 @@ protected synchronized void setConfig() { * @return dimmerId */ public int getId() { - if (config != null) { - return config.dimmerId; + if (this.config != null) { + return this.config.dimmerId; } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java index 92baabd4368e7..530e4dc2772a4 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java @@ -13,6 +13,7 @@ package org.openhab.binding.qbus.internal.handler; import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; +import static org.openhab.core.library.types.UpDownType.DOWN; import static org.openhab.core.types.RefreshType.REFRESH; import java.util.Map; @@ -24,6 +25,7 @@ import org.openhab.binding.qbus.internal.protocol.QbusRol; import org.openhab.core.library.types.IncreaseDecreaseType; import org.openhab.core.library.types.PercentType; +import org.openhab.core.library.types.UpDownType; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; import org.openhab.core.thing.ThingStatus; @@ -40,16 +42,15 @@ @NonNullByDefault public class QbusRolHandler extends QbusGlobalHandler { - public QbusRolHandler(Thing thing) { - super(thing); - } + protected @Nullable QbusThingsConfig config; - protected @NonNullByDefault({}) QbusThingsConfig config; + private int rolId; - int rolId; + private @Nullable String sn; - @Nullable - String sn; + public QbusRolHandler(Thing thing) { + super(thing); + } /** * Main initialization @@ -142,7 +143,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { } if (QComm.communicationActive()) { - if (command == REFRESH) { handleStateUpdate(QRol); return; @@ -151,17 +151,11 @@ public void handleCommand(ChannelUID channelUID, Command command) { switch (channelUID.getId()) { case CHANNEL_ROLLERSHUTTER: handleScreenposCommand(QRol, command); - updateStatus(ThingStatus.ONLINE); break; case CHANNEL_SLATS: handleSlatsposCommand(QRol, command); - updateStatus(ThingStatus.ONLINE); break; - - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Channel unknown " + channelUID.getId()); } } }); @@ -181,11 +175,10 @@ public void thingOffline(String message) { * Executes the command for screen up/down position */ private void handleScreenposCommand(QbusRol QRol, Command command) { - @Nullable String snr = getSN(); - if (command instanceof org.openhab.core.library.types.UpDownType) { - org.openhab.core.library.types.UpDownType s = (org.openhab.core.library.types.UpDownType) command; - if (s == org.openhab.core.library.types.UpDownType.DOWN) { + if (command instanceof UpDownType) { + UpDownType upDown = (UpDownType) command; + if (upDown == DOWN) { if (snr != null) { QRol.execute(0, snr); } else { @@ -198,16 +191,14 @@ private void handleScreenposCommand(QbusRol QRol, Command command) { thingOffline("No serial number configured for " + rolId); } } - } else if (command instanceof IncreaseDecreaseType) - - { - IncreaseDecreaseType s = (IncreaseDecreaseType) command; + } else if (command instanceof IncreaseDecreaseType) { + IncreaseDecreaseType inc = (IncreaseDecreaseType) command; int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); Integer currentValue = QRol.getState(); int newValue; int sendValue; if (currentValue != null) { - if (s == IncreaseDecreaseType.INCREASE) { + if (inc == IncreaseDecreaseType.INCREASE) { newValue = currentValue + stepValue; // round down to step multiple newValue = newValue - newValue % stepValue; @@ -252,7 +243,6 @@ private void handleScreenposCommand(QbusRol QRol, Command command) { * Executes the command for screen slats position */ private void handleSlatsposCommand(QbusRol QRol, Command command) { - @Nullable String snr = getSN(); if (command instanceof org.openhab.core.library.types.UpDownType) { org.openhab.core.library.types.UpDownType s = (org.openhab.core.library.types.UpDownType) command; @@ -321,7 +311,6 @@ private void handleSlatsposCommand(QbusRol QRol, Command command) { * Method to update state of channel, called from Qbus Screen/Store. */ public void handleStateUpdate(QbusRol qRol) { - Integer rolState = qRol.getState(); Integer slatState = qRol.getStateSlats(); @@ -339,7 +328,7 @@ public void handleStateUpdate(QbusRol qRol) { * Read the configuration */ protected synchronized void setConfig() { - config = getConfig().as(QbusThingsConfig.class); + this.config = getConfig().as(QbusThingsConfig.class); } /** @@ -348,8 +337,8 @@ protected synchronized void setConfig() { * @return rolId */ public int getId() { - if (config != null) { - return config.rolId; + if (this.config != null) { + return this.config.rolId; } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java index 969a425e0707a..71d3973b708de 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java @@ -38,16 +38,15 @@ @NonNullByDefault public class QbusSceneHandler extends QbusGlobalHandler { - public QbusSceneHandler(Thing thing) { - super(thing); - } + protected @Nullable QbusThingsConfig config; - protected @NonNullByDefault({}) QbusThingsConfig config; + private int sceneId; - int sceneId; + private @Nullable String sn; - @Nullable - String sn; + public QbusSceneHandler(Thing thing) { + super(thing); + } /** * Main initialization @@ -134,23 +133,16 @@ public void handleCommand(ChannelUID channelUID, Command command) { "Bridge communication not initialized when trying to execute command for Scene " + sceneId); return; } else { - scheduler.submit(() -> { if (!QComm.communicationActive()) { restartCommunication(QComm, "Scene", sceneId); } if (QComm.communicationActive()) { - switch (channelUID.getId()) { case CHANNEL_SCENE: handleSwitchCommand(QScene, channelUID, command); - updateStatus(ThingStatus.ONLINE); break; - - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Channel unknown " + channelUID.getId()); } } }); @@ -162,7 +154,6 @@ public void handleCommand(ChannelUID channelUID, Command command) { * Method to update state of channel, called from Qbus Scene. */ public void handleStateUpdate(QbusScene QScene) { - Integer sceneState = QScene.getState(); if (sceneState != null) { updateState(CHANNEL_SCENE, (sceneState == 0) ? OnOffType.OFF : OnOffType.ON); @@ -182,7 +173,6 @@ public void thingOffline(String message) { * Executes the scene command */ private void handleSwitchCommand(QbusScene QScene, ChannelUID channelUID, Command command) { - @Nullable String snr = getSN(); if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; @@ -206,7 +196,7 @@ private void handleSwitchCommand(QbusScene QScene, ChannelUID channelUID, Comman * Read the configuration */ protected synchronized void setConfig() { - config = getConfig().as(QbusThingsConfig.class); + this.config = getConfig().as(QbusThingsConfig.class); } /** @@ -215,8 +205,8 @@ protected synchronized void setConfig() { * @return sceneId */ public int getId() { - if (config != null) { - return config.sceneId; + if (this.config != null) { + return this.config.sceneId; } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java index 2b05ec07f59bb..1797f2a8c0fb9 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java @@ -41,16 +41,15 @@ @NonNullByDefault public class QbusThermostatHandler extends QbusGlobalHandler { - public QbusThermostatHandler(Thing thing) { - super(thing); - } + protected @Nullable QbusThingsConfig config; - protected @NonNullByDefault({}) QbusThingsConfig config; + private int thermostatId; - int thermostatId; + private @Nullable String sn; - @Nullable - String sn; + public QbusThermostatHandler(Thing thing) { + super(thing); + } /** * Main initialization @@ -140,37 +139,26 @@ public void handleCommand(ChannelUID channelUID, Command command) { + thermostatId); return; } else { - scheduler.submit(() -> { if (!QComm.communicationActive()) { restartCommunication(QComm, "Thermostat", thermostatId); } if (QComm.communicationActive()) { - if (command == REFRESH) { handleStateUpdate(QThermostat); return; } switch (channelUID.getId()) { - case CHANNEL_MEASURED: - updateStatus(ThingStatus.ONLINE); - break; - case CHANNEL_MODE: handleModeCommand(QThermostat, command); - updateStatus(ThingStatus.ONLINE); break; case CHANNEL_SETPOINT: handleSetpointCommand(QThermostat, command); - updateStatus(ThingStatus.ONLINE); break; - default: - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, - "Channel unknown " + channelUID.getId()); } } }); @@ -191,7 +179,6 @@ public void thingOffline(String message) { * Executes the Mode command */ private void handleModeCommand(QbusThermostat QThermostat, Command command) { - @Nullable String snr = getSN(); if (command instanceof DecimalType) { int mode = ((DecimalType) command).intValue(); @@ -207,7 +194,6 @@ private void handleModeCommand(QbusThermostat QThermostat, Command command) { * Executes the Setpoint command */ private void handleSetpointCommand(QbusThermostat QThermostat, Command command) { - @Nullable String snr = getSN(); if (command instanceof QuantityType) { QuantityType s = (QuantityType) command; @@ -227,7 +213,6 @@ private void handleSetpointCommand(QbusThermostat QThermostat, Command command) * */ public void handleStateUpdate(QbusThermostat QThermostat) { - updateState(CHANNEL_MEASURED, new QuantityType<>(QThermostat.getMeasured(), CELSIUS)); updateState(CHANNEL_SETPOINT, new QuantityType<>(QThermostat.getSetpoint(), CELSIUS)); @@ -241,7 +226,7 @@ public void handleStateUpdate(QbusThermostat QThermostat) { * Read the configuration */ protected synchronized void setConfig() { - config = getConfig().as(QbusThingsConfig.class); + this.config = getConfig().as(QbusThingsConfig.class); } /** @@ -250,8 +235,8 @@ protected synchronized void setConfig() { * @return dimmerId */ public int getId() { - if (config != null) { - return config.thermostatId; + if (this.config != null) { + return this.config.thermostatId; } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java index 7299c1da1dd4e..4e2b3b9ba70f5 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java @@ -15,6 +15,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusBistabielHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link QbusBistabiel} class represents the Qbus BISTABIEL output. @@ -25,16 +27,15 @@ @NonNullByDefault public final class QbusBistabiel { - @Nullable - private QbusCommunication QComm; + private final Logger logger = LoggerFactory.getLogger(QbusBistabiel.class); + + private @Nullable QbusCommunication QComm; private String id; - @Nullable - private Integer state; + private @Nullable Integer state; - @Nullable - private QbusBistabielHandler thingHandler; + private @Nullable QbusBistabielHandler thingHandler; QbusBistabiel(String id) { this.id = id; @@ -90,12 +91,18 @@ void setState(int state) { /** * Sends bistabiel to Qbus. + * + * @throws InterruptedException */ public void execute(int value, String sn) { QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeBistabiel").withId(this.id).withState(value); QbusCommunication comm = QComm; if (comm != null) { - comm.sendMessage(QCmd); + try { + comm.sendMessage(QCmd); + } catch (InterruptedException e) { + logger.warn("Could not send command for bistabiel {}", this.id); + } } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java index c8db92d174104..10c9a20f185c0 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java @@ -26,11 +26,9 @@ @NonNullByDefault public final class QbusCO2 { - @Nullable - private Integer state; + private @Nullable Integer state; - @Nullable - private QbusCO2Handler thingHandler; + private @Nullable QbusCO2Handler thingHandler; /** * This method should be called if the ThingHandler for the thing corresponding to this CO2 is initialized. diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java index a042f8f20fea1..be90cca920adb 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java @@ -53,22 +53,19 @@ public final class QbusCommunication { private final Logger logger = LoggerFactory.getLogger(QbusCommunication.class); - @Nullable - private Socket qSocket; - @Nullable - private PrintWriter qOut; - @Nullable - private BufferedReader qIn; + private @Nullable Socket qSocket; + private @Nullable PrintWriter qOut; + private @Nullable BufferedReader qIn; private boolean listenerStopped; private boolean qEventsRunning; private Gson gsonOut = new Gson(); private Gson gsonIn; - @Nullable - private String CTD; - private Boolean CTDConnected = false; + private @Nullable String CTD; + + private boolean CTDConnected = false; private final Map bistabiel = new HashMap<>(); private final Map scene = new HashMap<>(); @@ -77,8 +74,7 @@ public final class QbusCommunication { private final Map thermostat = new HashMap<>(); private final Map co2 = new HashMap<>(); - @Nullable - private QbusBridgeHandler bridgeCallBack; + private @Nullable QbusBridgeHandler bridgeCallBack; /** * Constructor for Qbus communication object, manages communication with @@ -97,62 +93,53 @@ public QbusCommunication() { * * @param addr : IP-address of Qbus Server * @param port : Communication port of Qbus server + * @throws InterruptedException + * @throws IOException * */ - public synchronized void startCommunication() { + public synchronized void startCommunication() throws IOException, InterruptedException { QbusBridgeHandler handler = bridgeCallBack; CTDConnected = false; - try { - for (int i = 1; qEventsRunning && (i <= 5); i++) { - Thread.sleep(1000); - } - if (qEventsRunning) { - logger.error("Starting from thread {}, but previous connection still active after 5000ms", - Thread.currentThread().getId()); - throw new IOException(); - } - - if (handler == null) { - throw new IOException(); - } + if (qEventsRunning) { + throw new IOException("Previous listening thread is still active."); + } - InetAddress addr = InetAddress.getByName(handler.getAddress()); - Integer port = handler.getPort(); + if (handler == null) { + throw new IOException("No Bridge handler initialised."); + } - if (port != null) { - Socket socket = new Socket(addr, port); - qSocket = socket; - qOut = new PrintWriter(socket.getOutputStream(), true); - qIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); - logger.info("Connected via local port {} from thread {}", socket.getLocalPort(), - Thread.currentThread().getId()); - } else { - return; - } + InetAddress addr = InetAddress.getByName(handler.getAddress()); + Integer port = handler.getPort(); - setSN(); - getSN(); + if (port != null) { + Socket socket = new Socket(addr, port); + qSocket = socket; + qOut = new PrintWriter(socket.getOutputStream(), true); + qIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); + logger.debug("Connected via local port {} from thread {}", socket.getLocalPort(), + Thread.currentThread().getId()); + } else { + return; + } - // Connect to Qbus server - Connect(); + setSN(); + getSN(); - // If Qbus Client is connected then initialize, else put Bridge offline - if (CTDConnected == true) { - initialize(); - (new Thread(qEvents)).start(); - } else { - handler.bridgeOffline("No communication with Qbus client"); - } - } catch (IOException | InterruptedException e) { - logger.warn("Error initializing communication from thread {}", Thread.currentThread().getId()); - // No connection with Qbus server, put Bridge offline - stopCommunication(); - if (handler != null) { - handler.bridgeOffline("No communication with Qbus server"); - } + // Connect to Qbus server + Connect(); + // If Qbus Client is connected then initialize and start listening for incoming commands, else put Bridge + // offline + if (CTDConnected == true) { + initialize(); + Thread thread = new Thread(this::qEvents); + thread.setName("OH-binding"); + thread.setDaemon(true); + thread.start(); + } else { + handler.bridgeOffline("No communication with Qbus client"); } } @@ -183,13 +170,19 @@ public synchronized void stopCommunication() { /** * Close and restart communication with Qbus Server. + * + * @throws InterruptedException */ public synchronized void restartCommunication() { stopCommunication(); logger.debug("Qbus: restart communication from thread {}", Thread.currentThread().getId()); - startCommunication(); + try { + startCommunication(); + } catch (InterruptedException | IOException e) { + logger.warn("Error on restaring communication."); + } } /** @@ -198,7 +191,6 @@ public synchronized void restartCommunication() { * @return True if active */ public boolean communicationActive() { - return qSocket != null; } @@ -219,8 +211,10 @@ public boolean clientConnected() { * and interprets all incomming json messages. It triggers state updates for active channels linked to the * Qbus outputs. It is started after initialization of the communication. * + * @return + * */ - private Runnable qEvents = () -> { + private void qEvents() { String qMessage; logger.info("Listening for events on thread {}", Thread.currentThread().getId()); @@ -231,9 +225,9 @@ public boolean clientConnected() { try { if (reader == null) { - throw new IOException(); + throw new IOException("Bufferreader for incomming messages not initialized."); } - while (!listenerStopped & ((qMessage = reader.readLine()) != null)) { + while (!Thread.currentThread().isInterrupted() & ((qMessage = reader.readLine()) != null)) { if (qMessage != null) { readMessage(qMessage); } @@ -241,7 +235,7 @@ public boolean clientConnected() { } catch (IOException e) { if (!listenerStopped) { qEventsRunning = false; - logger.warn("Qbus: IO error in listener on thread {}", Thread.currentThread().getId()); + logger.error("Qbus: IO error in listener on thread {}", Thread.currentThread().getId()); QbusBridgeHandler handler = bridgeCallBack; @@ -249,23 +243,20 @@ public boolean clientConnected() { CTDConnected = false; handler.bridgeOffline("No communication with Qbus server"); } - - return; } } qEventsRunning = false; - - logger.warn("Event listener thread stopped on thread {}", Thread.currentThread().getId()); - + logger.trace("Event listener thread stopped on thread {}", Thread.currentThread().getId()); }; /** * Called by other methods to send json data to Qbus. * * @param qMessage + * @throws InterruptedException */ - synchronized void sendMessage(Object qMessage) { + synchronized void sendMessage(Object qMessage) throws InterruptedException { PrintWriter writer = qOut; String json = gsonOut.toJson(qMessage); @@ -275,7 +266,7 @@ synchronized void sendMessage(Object qMessage) { try { TimeUnit.MILLISECONDS.sleep(250); } catch (InterruptedException e) { - // No reaction on error is required + throw new InterruptedException(e.toString()); } } @@ -301,7 +292,6 @@ private void sendAndReadMessage(String command) throws IOException, InterruptedE @Nullable String snr = getSN(); if (snr != null) { - QbusMessageCmd qCmd = new QbusMessageCmd(snr, command); sendMessage(qCmd); @@ -342,7 +332,6 @@ private void readMessage(String qMessage) { if (sn != null && CTD != null) { try { - if (Integer.parseInt(sn) == Integer.parseInt(CTD) && qMessageGson != null) { // Get the compatible outputs from the Qbus server if ("returnBistabiel".equals(cmd)) { @@ -352,7 +341,7 @@ private void readMessage(String qMessage) { } else if (("returnThermostat").equals(cmd)) { cmdListThermostat(((QbusMessageListMap) qMessageGson).getOutputs()); } else if (("returnScene").equals(cmd)) { - cmdlistscenes(((QbusMessageListMap) qMessageGson).getOutputs()); + cmdListscenes(((QbusMessageListMap) qMessageGson).getOutputs()); } else if (("returnCo2").equals(cmd)) { cmdlistco2(((QbusMessageListMap) qMessageGson).getOutputs()); } else if (("returnRol02p").equals(cmd)) { @@ -409,7 +398,6 @@ else if ("disconnect".equals(cmd)) { */ private void initialize() throws IOException, InterruptedException { - if (bridgeCallBack != null) { if (CTDConnected) { sendAndReadMessage("getBistabiel"); @@ -431,7 +419,7 @@ private void initialize() throws IOException, InterruptedException { return; } } else { - logger.error("Initialization error"); + logger.trace("Initialization error"); } } @@ -510,7 +498,7 @@ private void cmdListBistabiel(@Nullable List> outputs) { * * @param outputs */ - private void cmdlistscenes(@Nullable List> outputs) { + private void cmdListscenes(@Nullable List> outputs) { if (outputs != null) { for (Map scene : outputs) { String idStr = scene.get("id"); @@ -588,7 +576,6 @@ private void cmdlistrol(@Nullable List> outputs) { * @param data */ private void cmdlistrolslats(@Nullable List> outputs) { - logger.debug("Qbus: ROL02PSLATS received from Qbus server"); if (outputs != null) { for (Map rol : outputs) { String idStr = rol.get("id"); @@ -891,7 +878,7 @@ private void connection() { } /** - * Return all screen outûts in the Qbus Controller. + * Return all rollershutter outûts in the Qbus Controller. * * @return */ diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java index 60c0e5759f68a..6b539aeb60408 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java @@ -15,6 +15,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusDimmerHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link QbusDimmer} class represents the action Qbus Dimmer output. @@ -25,16 +27,15 @@ @NonNullByDefault public final class QbusDimmer { - @Nullable - private QbusCommunication QComm; + private final Logger logger = LoggerFactory.getLogger(QbusDimmer.class); + + private @Nullable QbusCommunication QComm; private String id; - @Nullable - private Integer state; + private @Nullable Integer state; - @Nullable - private QbusDimmerHandler thingHandler; + private @Nullable QbusDimmerHandler thingHandler; QbusDimmer(String id) { this.id = id; @@ -109,7 +110,11 @@ public void execute(int percent, String sn) { QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeDimmer").withId(this.id).withState(percent); QbusCommunication comm = QComm; if (comm != null) { - comm.sendMessage(QCmd); + try { + comm.sendMessage(QCmd); + } catch (InterruptedException e) { + logger.warn("Could not send command for dimmer {}", this.id); + } } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java index 17a9edc669126..41c3c013b761f 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java @@ -46,7 +46,6 @@ class QbusMessageDeserializer implements JsonDeserializer { final JsonObject jsonObject = json.getAsJsonObject(); try { - String cmd = null; String CTD = null; @@ -104,7 +103,7 @@ class QbusMessageDeserializer implements JsonDeserializer { return message; - } catch (IllegalStateException | ClassCastException e) { + } catch (IllegalStateException e) { throw new JsonParseException("Unexpected Json type"); } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java index 3b9389e999f22..b6190a23bb2e4 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java @@ -15,6 +15,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusRolHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link QbusRol} class represents the action Qbus Shutter/Slats output. @@ -25,18 +27,17 @@ @NonNullByDefault public final class QbusRol { - @Nullable - private QbusCommunication QComm; + private final Logger logger = LoggerFactory.getLogger(QbusRol.class); + + private @Nullable QbusCommunication QComm; private String id; - @Nullable - private Integer state; - @Nullable - private Integer slats; + private @Nullable Integer state; + + private @Nullable Integer slats; - @Nullable - private QbusRolHandler thingHandler; + private @Nullable QbusRolHandler thingHandler; QbusRol(String id) { this.id = id; @@ -124,7 +125,11 @@ public void execute(int value, String sn) { QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeStore").withId(this.id).withState(value); QbusCommunication comm = QComm; if (comm != null) { - comm.sendMessage(QCmd); + try { + comm.sendMessage(QCmd); + } catch (InterruptedException e) { + logger.warn("Could not send command for store {}", this.id); + } } } @@ -135,7 +140,11 @@ public void executeSlats(int value, String sn) { QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeSlats").withId(this.id).withSlatState(value); QbusCommunication comm = QComm; if (comm != null) { - comm.sendMessage(QCmd); + try { + comm.sendMessage(QCmd); + } catch (InterruptedException e) { + logger.warn("Could not send command for slat {}", this.id); + } } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java index 2a4b3db682e1a..1e7f8d38f26d7 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java @@ -15,6 +15,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusSceneHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link QbusScene} class represents the action Qbus Scene output. @@ -25,14 +27,13 @@ @NonNullByDefault public final class QbusScene { - @Nullable - private QbusCommunication QComm; + private final Logger logger = LoggerFactory.getLogger(QbusScene.class); - @Nullable - public QbusSceneHandler thingHandler; + private @Nullable QbusCommunication QComm; - @Nullable - private Integer state; + public @Nullable QbusSceneHandler thingHandler; + + private @Nullable Integer state; private String id; @@ -58,7 +59,11 @@ public void execute(int value, String sn) { QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeScene").withId(this.id).withState(value); QbusCommunication comm = QComm; if (comm != null) { - comm.sendMessage(QCmd); + try { + comm.sendMessage(QCmd); + } catch (InterruptedException e) { + logger.warn("Could not send command for scene {}", this.id); + } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java index 661f0705e9503..f874c2d3485bc 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java @@ -15,6 +15,8 @@ import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusThermostatHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link QbusThermostat} class represents the thermostat Qbus communication object. It contains all @@ -27,16 +29,16 @@ @NonNullByDefault public final class QbusThermostat { - @Nullable - private QbusCommunication qComm; + private final Logger logger = LoggerFactory.getLogger(QbusThermostat.class); + + private @Nullable QbusCommunication qComm; private String id; - private Double measured = 0.0; - private Double setpoint = 0.0; + private double measured = 0.0; + private double setpoint = 0.0; private Integer mode = 0; - @Nullable - private QbusThermostatHandler thingHandler; + private @Nullable QbusThermostatHandler thingHandler; QbusThermostat(String id) { this.id = id; @@ -146,7 +148,11 @@ public void executeMode(int mode, String sn) { QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeThermostat").withId(this.id).withMode(mode); QbusCommunication comm = qComm; if (comm != null) { - comm.sendMessage(qCmd); + try { + comm.sendMessage(qCmd); + } catch (InterruptedException e) { + logger.warn("Could not send command mode for thermostat {}", this.id); + } } } @@ -159,7 +165,11 @@ public void executeSetpoint(double d, String sn) { QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeThermostat").withId(this.id).withSetPoint(setpoint); QbusCommunication comm = qComm; if (comm != null) { - comm.sendMessage(qCmd); + try { + comm.sendMessage(qCmd); + } catch (InterruptedException e) { + logger.warn("Could not send command setpoitn for thermostat {}", this.id); + } } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml index b18daa90afc8d..2457b9d1b1a7d 100644 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/binding/binding.xml @@ -4,7 +4,7 @@ xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd"> Qbus Binding - This is the binding for Qbus. - Koen Schockaert + This is the binding for the Qbus home automation system. Qbus is a system made and developed in Belgium + (https://www.qbus.be/nl-nl) diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties new file mode 100644 index 0000000000000..bca15973e1883 --- /dev/null +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties @@ -0,0 +1,99 @@ +# FIXME: please substitute the xx_XX with a proper locale, ie. de_DE +# FIXME: please do not add the file to the repo if you add or change no content + +# binding +binding.qbus.name = Qbus Binding +binding.qbus.description = Deze binding maakt via een server applicate verbinding met de Qbus controller. + +# thing types +thing-type.qbus.bridge.label = Qbus Bridge +thing-type.qbus.bridge.description = De Qbus Bridge Maakt Verbinding Met de Qbus Server. +thing-type.config.qbus.bridge.addr.label = IP Adres of Host Naam +thing-type.config.qbus.bridge.addr.description = IP adres van de Qbus server, meestal 'localhost' +thing-type.config.qbus.bridge.sn.label = Serienummer van de Controller +thing-type.config.qbus.bridge.sn.description = Serienummer van de CTD controller +thing-type.config.qbus.bridge.port.label = Poort +thing-type.config.qbus.bridge.port.description = Communicatiepoort van de Qbus server (standaard: 8447) +thing-type.config.qbus.bridge.refresh.label = Refresh Timer +thing-type.config.qbus.bridge.refresh.description = Ingestelde timer, bij het verlopen van de timer zal de communicatie met Client/Server gecontroleerd worden en indien nodig herstart. + +thing-type.qbus.onOff.label = Aan/uit +thing-type.qbus.onOff.description = Alle Bistabiel-Mono-Timer-Interval uitgangen +thing-type.config.qbus.onOff.bistabielId.label = Qbus ID +thing-type.config.qbus.onOff.bistabielId.description = Identificatienummer van de uitgang (zie SMIII) + +thing-type.qbus.scene.label = Sfeer +thing-type.qbus.scene.description = Alle sferen +thing-type.config.qbus.scene.sceneId.label = Qbus ID +thing-type.config.qbus.scene.sceneId.description = Identificatienummer van de sfeer (zie SMIII) + +thing-type.qbus.co2.label = CO2 +thing-type.qbus.co2.description = Alle CO2 Uitgangen +thing-type.config.qbus.co2.co2Id.label = Qbus ID +thing-type.config.qbus.co2.co2Id.description = Identificatienummer van de uitgang (zie SMIII) + +thing-type.qbus.dimmer.label = Dimmer +thing-type.qbus.dimmer.description = Alle Dimbare Uitgangen +thing-type.config.qbus.dimmer.dimmerId.label = Qbus ID +thing-type.config.qbus.dimmer.dimmerId.description = Identificatienummer van de uitgang (zie SMIII) +thing-type.config.qbus.dimmer.step.label = Stappenwaarde +thing-type.config.qbus.dimmer.step.description = Waarde gebruikt voor het dimmen in stappen (standaard 10%) + +thing-type.qbus.rollershutter.label = Rolluik +thing-type.qbus.rollershutter.description = Alle Rolluik (ROL02P) Uitgangen +thing-type.config.qbus.rollershutter.rolId.label = Qbus ID +thing-type.config.qbus.rollershutter.rolId.description = Identificatienummer van de uitgang (zie SMIII) + +thing-type.qbus.rollershutter_slats.label = Rolluik (met lamellen) +thing-type.qbus.rollershutter_slats.description = Alle schermen met lamellen (ROL02P) uitgang +thing-type.config.qbus.rollershutter_slats.rolId.label = Qbus ID +thing-type.config.qbus.rollershutter_slats.rolId.description = Identificatienummer van de uitgang (zie SMIII) + +thing-type.qbus.thermostat.label = Thermostaat +thing-type.qbus.thermostat.description = Alle thermostaten +thing-type.config.qbus.thermostat.thermostatId.label = Qbus ID +thing-type.config.qbus.thermostat.thermostatId.description = Identificatienummer van de uitgang (zie SMIII) + +channel-type.qbus.scene.label = Sfeer +channel-type.qbus.scene.description = Bediening van de sfeer + +channel-type.qbus.co2.label = CO2 +channel-type.qbus.co2.description = Uitlezing van de CO2 waarde + +channel-type.qbus.switch.label = Schakelaar +channel-type.qbus.switch.description = Schakelaar bediening van de uitgangen + +channel-type.qbus.brightness.label = Helderheid +channel-type.qbus.brightness.description = Helderheid bediening van de uitgangen + +channel-type.qbus.measured.label = Gemeten Temperatuur +channel-type.qbus.measured.description = Uitlezing van de gemeten Temperatuur + +channel-type.qbus.setpoint.label = Ingestelde Temperatuur +channel-type.qbus.setpoint.description = Ingestelde temperatuur bediening van de uitgangen + +channel-type.qbus.mode.label = Ingesteld Regime +channel-type.qbus.mode.description = Regime bediening van de uitgangen +channel-type.qbus.mode.state.option.0 = Manueel +channel-type.qbus.mode.state.option.1 = Vorst +channel-type.qbus.mode.state.option.2 = Economisch +channel-type.qbus.mode.state.option.3 = Comfort +channel-type.qbus.mode.state.option.4 = Nacht + + +channel-type.qbus.rollershutter.label = Rolluik Bediening +channel-type.qbus.rollershutter.description = Rolluik bediening van de uitgangen + +channel-type.qbus.slats.label = Lamellen Bediening +channel-type.qbus.slats.description = Lamellen bediening van de uitgangen + + + + +# thing type config description +thing-type.config.qbus.sample.config1.label = +thing-type.config.qbus.sample.config1.description = + +# channel types +channel-type.qbus.sample-channel.label = +channel-type.qbus.sample-channel.description = diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties deleted file mode 100644 index 0c7193b3f16c7..0000000000000 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_xx_XX.properties +++ /dev/null @@ -1,17 +0,0 @@ -# FIXME: please substitute the xx_XX with a proper locale, ie. de_DE -# FIXME: please do not add the file to the repo if you add or change no content -# binding -binding.qbus.name = -binding.qbus.description = - -# thing types -thing-type.qbus.sample.label = -thing-type.qbus.sample.description = - -# thing type config description -thing-type.config.qbus.sample.config1.label = -thing-type.config.qbus.sample.config1.description = - -# channel types -channel-type.qbus.sample-channel.label = -channel-type.qbus.sample-channel.description = diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml index 3311fe5866eb4..fd68182c27f35 100644 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml @@ -9,29 +9,26 @@ This bridge represents a Qbus client - - IP Address of Qbus server, usually 'localhost' + + IP Address of Qbus Server, Usually 'localhost' localhost - false network-address - Serial nr of the CTD controller - - false + Serial Number of the CTD Controller - Port to communicate with Qbus server, default 8447 + Port To Communicate With Qbus Server, Default 8447 8447 true - Refresh interval for connection with Qbus server (min), default 300. If set to 0 or left empty, no - refresh will be scheduled - 300 + Refresh Interval for Connection With Qbus Server (min), default 10. If Set To 0 or Left Empty, No + Refresh Will Be Scheduled + 10 true @@ -42,15 +39,14 @@ - Bistabiel-Mono-Timer-Interval output + Bistabiel-Mono-Timer-Interval Output - + - Qbus IP Interface Output Object ID - false + Qbus Bistabiel ID @@ -60,15 +56,14 @@ - Qbus scene + Qbus Scene - - Qbus scene ID - false + + Qbus Scene ID @@ -87,7 +82,6 @@ Qbus CO2 ID - false @@ -97,19 +91,18 @@ - Qbus dimmer output + Qbus Dimmer Output - + - Qbus IP Interface Output ID - false + Qbus Dimmer ID - Step value used for increase/decrease of dimmer brightness, default 10% + Step Value Used for Increase/Decrease of Dimmer Brightness, Default 10% 10 true @@ -123,16 +116,14 @@ - Qbus Shutter control + Qbus Shutter (ROL02P) Control - - Qbus rol Id - false + Qbus Rol Id @@ -141,8 +132,8 @@ - - Qbus Shutter control + + Qbus Shutter With Slats Control @@ -150,8 +141,7 @@ - Qbus rol Id - false + Qbus Rol Id @@ -161,7 +151,7 @@ - Qbus thermostat + Qbus Thermostat @@ -170,8 +160,7 @@ - Qbus IP Interface Thermostat ID - false + Qbus Thermostat ID @@ -179,42 +168,35 @@ Switch - Scene control for Qbus + Scene Control for Qbus Scene Number - CO2 value for Qbus + CO2 Value for Qbus CO2 Switch - Switch control for output in Qbus + Switch Control for Output in Qbus Switch Dimmer - Brightness control for dimmer function in Qbus + Brightness Control for Dimmer Function in Qbus DimmableLight - - Color - - DMX control for dimmer function in Qbus - HSB - - Number:Temperature - Temperature measured by thermostat + Temperature Measured by Thermostat Temperature CurrentTemperature @@ -225,7 +207,7 @@ Number:Temperature - Setpoint temperature of thermostat + Setpoint Temperature of Thermostat Temperature TargetTemperature @@ -236,15 +218,15 @@ Number - Thermostat mode + Thermostat Mode Number - - - + + + - + @@ -252,14 +234,14 @@ Rollershutter - Rollershutter control for Qbus + Rollershutter Control for Qbus Blinds Dimmer - Slatcontrol function in Qbus + Slatcontrol Function in Qbus Blinds From 35f160764de231acf7b9f3e92e3d51df37300a8f Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Wed, 24 Feb 2021 16:31:04 +0100 Subject: [PATCH 12/17] Updated code Updated code as requested by FWolter on 23/02/2021 --- bundles/org.openhab.binding.qbus/doc/Logo.JPG | Bin 12981 -> 909219 bytes .../qbus/internal/QbusBridgeHandler.java | 50 ++-- .../qbus/internal/QbusHandlerFactory.java | 1 - .../handler/QbusBistabielHandler.java | 55 ++-- .../qbus/internal/handler/QbusCO2Handler.java | 54 ++-- .../internal/handler/QbusDimmerHandler.java | 85 +++---- .../internal/handler/QbusGlobalHandler.java | 30 +-- .../qbus/internal/handler/QbusRolHandler.java | 80 +++--- .../internal/handler/QbusSceneHandler.java | 52 ++-- .../handler/QbusThermostatHandler.java | 67 +++-- .../qbus/internal/protocol/QbusBistabiel.java | 19 +- .../internal/protocol/QbusCommunication.java | 239 +++++++++--------- .../qbus/internal/protocol/QbusDimmer.java | 18 +- .../internal/protocol/QbusMessageBase.java | 8 +- .../protocol/QbusMessageDeserializer.java | 12 +- .../qbus/internal/protocol/QbusRol.java | 26 +- .../qbus/internal/protocol/QbusScene.java | 18 +- .../internal/protocol/QbusThermostat.java | 4 +- .../resources/OH-INF/i18n/qbus_nl.properties | 14 - .../resources/OH-INF/thing/thing-types.xml | 73 ++---- 20 files changed, 448 insertions(+), 457 deletions(-) diff --git a/bundles/org.openhab.binding.qbus/doc/Logo.JPG b/bundles/org.openhab.binding.qbus/doc/Logo.JPG index 4db5d75ae4403d9d1aa945287c333e9dc85d6130..93f10e4de88ffea9877a7fed8cbca1c4f062c5b6 100644 GIT binary patch literal 909219 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

O}2C*lmW!ScEVi&?-~fA2e8c6 zp{(eSV@=V4^gSz-Xe}M*@B-USH$TK}CJ7U!GFy)b;(ATnZ}3y3W1vA^6)hN2!xcjH zFgwoSYASq){a@A%q#HXv28&wH0#6s5v#9&hPY40q$(F3Da> zQaZNGZmr*gN#gSmHgt`stV#>JCwP;!0#U_RtoaWqFEH!^L497FIf1dePLG%=~l-t0|43%zN8(kbcG63AJ+ed zFI3d5i9~FW+4>wpC?$z=kD{G;VS6z3_qRM9A8AUQTw*xWf*-9C+--vm&Jdq;l=t~{ zJOGDvO>XRfyf+lBC&TEo9;HX{-Kt!lPK3EUb?zI~Fkia6r9O4H%h*ukmTgn|8_ln$ z%J^SfA5G3DI<=o2dDh_v8XGuXaTNmUIlX=!^smu6x(gns+w4Pu^OeWv%|qo1f_HK2 zrjKkGz1$eGKTYq{OxTsjk8Op{zzFT_m7Dw8)u1(FcPeVYzlRbtcS1G2FQQvuSB)5- zX1K5V-+8N1R)VDcPtl=GS(}$*ELt9ShhfgN*NMMiS)gf(9G3*S46(=khCQkLf&T|_ zdqW;Ug4!N^p12OvxIBqCiDlYR$!@sOy;BGs8Jdp$qW^ zFBTj`*%DHShtRHsZyiGz58~#EkJtcGUFHZbkZc{L#+#81-a32{`GMUvq6-DQlMCsi zeH<--EuwwXeSjlqzxf4-0lEc&i9Ac6+pa;qqq|qk!#LCZGCpEg(!-+M~BS8dRc_}8Ps2Lj?)RFiOD!m_=VZ6H5chB z_+D0qs^XtW*P+?Gk;vs35Vzaw6-LQrzzxkW@_&8w(v) z1!LXemWsrdVMMiTV`&$1QX)*ZLfsYJh&Y5kE-dmohJGL@wV%eV=2%THZ(lJG*k94{ zxVKq}0@J!?**}mMo#vR8umT;d`4W6u{iNg#(ppJKqoWe#krAzEe`%CgFM6$*Z2ts1 z$`x;U&?cC)?DOeJ9vxHUgKiC#G0Pxn{m$q$(5ddn&F1h0oqJ1C5m4Q(G!x{6`boqB zluilpqM$})DGqb7Yj{^SpKmkU_OaK!efQKKc`2x1lQRPW-ZvJD0zp>|L7NI;!oG>( z8TiX?$5bWK-!K(^7PVMAp8`?D2B+}(I&;0*CY z3qtjb&fd;T@DHjI{@w!om94R8m>E z?^6_fAb4NjFZ^aGMHxv5f-h&86UGp|=xs!I)RN{bq7bbv2_?&kHOd%8RZ?4C>V`#l**c;duIRwYSr7_reFT4v1grC8`Y6>MJ5sntGCp;nQ zQ}z?Nq?WKYk^^~(#}ZN;8E^A~tfU|&yU_^h>h3B`F11Sf3hPGY0wLJ*)KO$IjzqoQ z7>@U%*%#sQH)ts-?gTLn8~Tg5l-9fKEir-CXX{P+hqi6}0`f2CkZ~{SHz!4cK|f(1 zp*_M(uwxMJSP<)2Lo{v$Yh&RE?k)36G6_G$REJ6lsm%Cg#|eduPqxh@6X3?!YQ$lw zV`nt-k@&560qUvf6m%&ty z&9)Q7bmqm8gYbW~ApLX1XLXQES+WN$$TmM9KMD#K+9 z4~3k;JrFRLF>uHDn{A&HhS|U{25#4LNjHTE@9Gd7LMC;Zkwqv!ogTUh?WoDEoj|`= zEyx#OZpyzTUc*jH|At_3sgjaq4LEm^!p@DbjPqeA9mXBa((>TlL*@Ko#6W)|Nr05} zbU;#2Sfi-sDY{g@KkpePMLRFC9J^Gd3h}~DD;UcXu?(q)T{?b}+cOvfn=^G*!-BhS zD&}oNc#d5oP9W`ui@+4*=l;hvaj45ZKwbs9^Z(j-Aj(IsyE+c7k36v(68t zXCkZ_myce7?bA`LUwWdBF{#rVFq zk6g&u69|xn46a)%8NxtV@1qm}if%0au}C1VBoKvrSFxJ41q^B&qHk>0Z{I}hQqjYi26 z+zW#OxP=GnXlOqEf~KbB2O&aLQISPZDjGNZApDgv|aj5fVP8Tri`nX!BW86s!Cw2{lB0N$ywT<{F^RLK6V0n{w3F=T%A5Ov zJ)E;`i_j??_l}Yl6`KHNmmgx?hfZ%;%xZzp#Llp6kqZAN<`s0R%R$y$OtYng@gDzH ze}&@-S|g5U_d@2;E7I_@x_@2#!5oY z>_?Lq#5lh@z)q60%ROM4G%^PUI8*vHdd5YPi6D+4BGD`vT-?|OD9Oi4 zt$**I|mdqHjzsKcZL)O z(62Bm8(QfyMqEh%J(saEBZ>4K0EVcDYTxz|OP+ zQI9ZJDbi?k`D0EJjV{|h>yCm*i4Y`ht;D}hLt7yFRHUaq6P`@_M!hJQh~7ut#~)vH zgv#f=b-6&b;MrMCP#3fM<<8XedIj5;dP#egU`9QraR3XcovMi1GgQ0+Rrs1(C_9qY zLR}~IiO!;~6i=_hQC^7#UG`IQ1X!zw6dt=+W=W0hTgh^#X7t$Lt*8mctDs5hA_KkF zhVn(Xt}uggQDdKald@O&G3pwnOR->;H>FHQap@tCh_u%8DD&7T$v4WG(FaU_%B|r- zTnOdjz(mJu%4qL{nnnt*+p}N~rMVN9T1Uy!J&1x+d^LS5uamDSce}92ZjvhN2=WA5 zB>qLAOvGgG@^Qn(l?F0I zlkWm0jY#TScqv@Hfr3uPU7HP;0cFs0xI9|s*x`H$BH zABimEen2u-9OR17p$o>j-!TQ|57=>pbCSE_>W*QipC|x4fb$YMLa`k>ffL-W`VQX< zF_iz87lTSoDdZy1CnH{P&SKZBP;%;V`~^2Sop^*rH7ktlD9#k_K&}Gz2nJA0%pHCo zdS5$>w-a-?>J9e}_F{fG*9P}2c{`^B|2BfbK1g`I0>UmN-gY)&H;^P2rPQV%zzzqj-Tj3lK5MQ~BJq{%1ZeqOw@{)M0DxfrcFY_7D zxMJ}vy@T((hM54|vt$FK0F58deb4iweBv(UK18;0inzoUJX^zgwlS6UfYX}uffdC5 zocNB}$7Y3RGyT{reQwQiOa3^oV%hQM#b}rZP%mHV<3|5S z%5n~(%@Qc)uxJZdZLAOMhM@|gIcsnr9D0-as`pi6F*BwcRmNi&j8<9Qz#si&!XE&w zMTH)vzf`l9-=L=|>YUwZwW3FJCaDuFCd-obck(_Vk(E9k2#sb+N7@>WGj0#E%N!VM z`$n^(fr0J^2}6LZF*6iT=jmgX_t9>t#+~h{FU68MXQ<^YJ-bQspwWbwsQS_@h2B>B zw0>`Nl9M|8%N(Wu16OQ#Dd~W6*WMJbfU|=?i@cFFUUP*awB5Yx!Y)jUSuHn{sA0`f zJ?}^%xF}bF!y&iiOsH9dkIWp_U%FNzL9E{pDPDp6w6;+A77YXs3*KYCdzlNea7B(w z1%7yj*?mqVxt}qn%ttifZpf#Q2f+tq-e{Bh2+0=Aq7tY$3%h1LSNI9n5Pwl{8czwX z;$I_3Jb&>0h)TyQ-W^hxc_BNAiURQRmv|(0T2?|>3!0EzC48()5~GQnVxwp&DKPVf z;2i00d;xzK83FdE9*A6p+pEtBt_ZFc68J`eO@C5@ zSj&T{5NUt7>lN2xtvMO;vOom8MCR+cm{lvOb9~QSDzLZoraxlhNo&N8ec$0dqF-!#9V&S!9k1b57 z;*H}&hNG~<5=Qr6P7)2>ghoqvfN@1LvYD^H*ZR*!g?7CCb6&Gr0rpLGQYxY4(I@2( z;1<GA=UIy+wK)b=#gM8NnPgb>z<{rOuM}Hn+cqtu?fR4mR!6#X}esmo;IqpxnKx zaQLGXmLdhIk8Y6}(4qbqX(DE;J4MohbG5%E_Qc;cRdGL4yz%v&uJCDSxb7R`Pva@g z6I5JTmB<8A!cOBNA=+~s0_VwS^d;Zf2jGh5Dg>Iqz> zemAZP5~l6LFK@V^HsBwWhs}bvIfT}8kD2&vL*^{z^G_R!dlGik9RD`&JcG>T- zXdyk^ykQ0c}b*a9`qX&rZZ2w%LJ=fD;#d{3YlBXSFZpn<{Bxzg zvY-4_S#oIzPnGyc4COgSQiLzLJ$_JuoLl40;Ma2T4!^n0>@DUvW;df8g;sx(tJ`<0 zY-EDk(+XcHr9>plk$|%tBz!SA5hXe=qDJ}%bA+4xg81)*9`2322?2VpGe^aRn(t!# z$6z6+)b+XrZ6B0a&EZ-*xlG+y{9HPt{ImfOe^$s7O+-oZw1^{ugR(`d?(-|9k?tWp zYq9rSH})OgSM#?FG-C|$RCT)hP+O$pL)YAz2Kj={qT(`Xh@QW}N{rE-S=T8%p;;9H z7No02R%v;673bala;egqxl7rpe4&LK!-DZI!bi1csHat_APqEB-;z!Cg%&-M9PgRG zeo*XS{JUm*-fu@0DE+=sm$&S+fU;yjSk zJX7V^i*Nm1aH8vG$C8ZV&Ju7^{2|>NC@3^d^8^m^d8N)lrnt1K2&hLk9rAV99upG* ziS!O;J}7Cw++5Z-2%4@$c0YsQ3nGneu%+o&^xxoT<14fVBrDWXtw8(u+*7{6Y;oyQ ze89QbT#-%^wwu8CbczD%)&B*a-URf9Ax1V%cS(^S^VOY;(M!|wbz3nNaql!W*q)Fh zs&d?2pYw`I!a|obayc>CHdK;H=9oO^QE1MP{5}KL*f`Vu7{{x4W6Z)gIjipc6o)$g?Hkj_ftyB59)XVPql)dZYs@90{sme)lY`c$0KJ34=5 zdrqqQydoq8P%7keVfxL{N)qni>{RMC40yaZC2 zDIV~FeoMID8wVeXT-pUjR{1LoztC15QF;qZkJGgFBF@PwSpJA8H8J5orIs|=P58n- zZhSIw2a#XUIk+3ynYpa*9O_g8xBD%|F5<4y8(Z#=(Qm?ydZcRy@n4*NX|jm1RvHb08AN#$)egK8LpV8kd!%rl5cTE%65lX{jBG^Aq2Pc(#c|7Rj+`)5c^OI$o$}NR{FBnzNGY ze!Z#;NuEcl{GB+$*;IO4fV7DelyPEBKd|Ql`Lz!QiTa?j&_1yCYi?aPTC+Q?qSL5G z#V6|?s|vzbYSt*L{cy@@MTAGNJYRa-*-Gj!^0O)CZ|CHhd9m~WqBeivMNd%a(cULr zq+GkM51n_?oH`d7D&wtmje7I2@9KlvQNL1Uw7SS+ne4p$m$SX3Q_Qo0^20fGW|^#5 zK)BI&C#1=zAZKQOi$i9|)Uwv!>(GSX|(_)fvRijNdGVSgy;SmXQa~E1Pv{_V$;k=*nj!wrU3ZQjE|eGiopr{i zM#T+Bpe2EY{kJjCm+$F$fXiFFv8xroXKuS;4^dzlrNC0Svk4pNCKcXWNtjdlOZS;-MGMn>!Z1Nn<$6DyZBFm@qJ{%tL2BgX|vo0Ugv*ghq?Ro8I*2Iy*!+To=xzj zo0Jbt7m)Dz7Mos^&SaF2bIG9z`VnvPbmYT<8A?*1Z*Lyu;qp^m8>qU)yA3lmo6^!FJH#{l}*gu3D7fLr9w{@uXFK&PHEU~2gl zV;W=C;#Yb(N?RIPg5?#B27i^gE+n?7oDcp>#Gh zQq}j6T^(rJO=m-v-|cK*9&jt#ZK4= z9ny^>hXu83e++{8cOt=kbNS4G7hT)7qG3eg<94-(=7Hh{Xwa5(!-dPn>u>kz)HI}#MPUwt?3Wa zUX6Iy{YLXRK-{@qOicGF+b zfriUn2Ovew{bC$+N81L94SadWUN8wEhPYOeksn|i*MCMQA*j((OcknqxjE(rdcbZy zUWOUod%WdI`^3ckwpXC;&gUKHAspdFFdbG+(nEa_^FisbBgowwQHUaR*7^m=6PV*s z_fbD^3%ongwYb}M%{T~tVz;nqB`R_3S_>VWW^id6!{iFmI(XPBVmi1ISKhG_x*VTd zL5JNUL}X?lAViy}B;*wFfcH9-8EKIn4GSWT>}1wICD)IdHFc1?bl8@||YJsd@0TTVTwO7AV}`&1j)wOjGM{&A-t^I21#ZVP&< zWl)pR+}QR+`MgBj@k2hC77jiq1tL~K!^A4DMrgjs$$kp?hcmflOC@eBv-eU>)3B5L zfA!1ypE3T|DCs$iI@|m|C{H=y~}!>tNnIN$u`t}*;)uRG_-EEL&BO)jQ!h)X>}gr z7F)E(_n}f3gZ#Ss!kOS6{Wh;K@KGhzAp-G*_hyCxx2-K2q$4~Va@560zvf`}Q z3v4#px1*^=hFJ|hRT_(31Dl_gj*CY`g=6qbP+lGkJOf>BlT5P0e45Sx|848<=R-R> zW|T_UaqxE5dW1E!2mKJ43~y_dkzPQd&y#V8G2gatLFA)eu=#RaHDJSXJ)0GPp4f&LiI{A|w7# zl2aC;$|*ge&(O`37t0Q!1(dfoH}Edhl!<~C3A43pL7RrTN;1`fXS}2@1bZ_k5s8pv z40OYKSQTSk;UxTj4C`bxvJkisNtTAAZ{Ky30-Xw>i*oNARsRO*cs1*Y2gkKyx~Ns>*7Qflnxs@;jjvSxVv~ zxT&N$gn)1t9a~m{un}&s{eb~Eo39?EDUyAR%BO!-p!pHI8|@D z(_UlWm@#9?i*0V%VoOYG&6@BAFK-VRNvM9)@pqsh&kKC4XKLLUXr(bd_yUZfYg*<5 z11VP7k z5rE$f4k9f@{_A#`ghKtWDyGz9tA~40sMddV*U|ay{}Y_U_<=r?8!*2icOX}=$6#A) zSK-(QX5JxuDJo&zC}9QqLC|i(d2GJhCZY|_$7-0IOUNDMB5dI}&10k=;uwD}Di&o+ ze2b1kr-C(@z|bnjJMh)VmFgM!*Wh3y3zlvVPpBwNV{hsIT7LDG)rQ0mQcd)#TyW6+w-${>xnst_p ze(-nA5=;WLN)^!B4cnzCuDFkw-Hl|ykcE;<@l7b4I5uDuohW?fwi5M2P-9bud&)L+ z_O)B}hfAhArg{zo-QdKoY;+-H+;E}g0?by|Q4tES(l~B-h!|I%jr)$gBHt0vgfhxZ z+!mq=MbB-7I1C4Em__`Jqr?X~7L8Ephd?8POcWooun%bQfa1DExAfIyKGAU-5%sWF@}5w?i}@OmO=gu zc9;9W^85TY%z+Pf-H9tgTru2P{S(=(dAH;{vO+dva|3&xGuqWneqLjc3CX|fJDEuo z=OzrMhZ5Ynp!F!Fq+MNcfC7N+H@u)6g3ek&sdjLT{}pOEvclDxI)q|aifBUY-OhVN z&sKZMbK7tgQU4ux7M3SofoH+@(tQcbkxP(|2)(Gr=4zrFdaP_KQHgoGK7-_lTM-*T z;^Ei(N0FWoHn`f6-V!d)DWRyyHo5}L|8QX87ZLGkdXwORi;9cmJ8L#pG(%oXh2w-}E-#FneJjiHck?jBH;$j?1)zB z+G{_c*J^T#Trp0n_%tEro?=~e16C-5ui{~!OD$aPV(G#$Ya7A{8zv*d^ut{&H2n0S zD?SJDu#XM;fV|Lsv$g=W-567-K#TQ*X>OPb?doV7thwsLsw`}Ug70z`^G)1o9YI*n z7D@Fm*UehyR(Spd6*q(ckJf@Jku^h6HRn<3{qqZb(H`ADQxBtG8||YGW0d;xRZB3> z)RiuWFnp<>bqW3tyI%ogp08=-R5EMpXU*h{Z%wNp$qZx5!MY?yUVCcMB8DC4P?|OH z5NaJ244i{|`!)hMktWW|7zwCl=C6Qh98Y!}h-d+^t^uBH{rDchykjru7yTx9tag^( z2vruk(YxW4)O5NSnGrcn=b%3NhS0?ruyY#y2sYEAnLa{vl-khdLNl33v;f#i-0W5Y z5!Z2s7KK#RxYE|3-W05*#bYv3I%(-xNn|pu2zS`moK}m!=-fsF6TlXiX#2=HVm&1e zYX!WcAhEBp)|5`%vG&81X*{nwY_>yEoKK}=PA|6u1NwO7psCncd=UM1F zS!@|feM3zXEhmjq2Iy+if0Uo-bL2#7O4}wfgo>+5ChMrX^0tx>QU4^jl7G>nA`p}) zTHA_2N(rsoSx51sUAGibm(%?Odx=SGGg>n780!*BLkeW6S`8!^E2%P{G|v2)`MVF-GvXWr8Rc zSZt&buk#~vLr6<_(4=5eGj~(Cgfz%CS+Sh7g(G(EBbBf(&RIo1$b|8t@Wsl{_!jo+YqIUsW6>dP+1oaw0^Wow$j(Pc$#wpZHzy#pfyUmmt!)i>T+t&uJ!y zvx>OUxW3L>vJHN%ej2_Be^mRv*^aPM<5b~FV5t&w%n8>Ogv3aquk2XZzr-qOqE8>O zNc_iHM))kKo1-V~XJI*UxOx2#Nvm;Fz5l>o<72v2O%3=SV^TSfV519LmiY!x z<7D$Pn%|KkxPrEH*jdh<4s_!?PAa&i>;`)VTAJ0x_JgMYjo2KskBnQy)2ywW4Uk&GG4@WFGgQD1fR8rZXKhB>m%d}gpimobFi)ZHto^`5 zU_(PTFoSUjuNkI2Ug3C&@s|KHuLYiwFEh(oYV>|Qmo*P_2Lfeku@CB>F~e|sN?jP2 z@Ld~LFfau6S}-GuSP}e-VMp@x>IL4CZp}9V&XPIihX6-vC&Pj1M7F~fFb2t;;6g?U z#kuYc@R`z9GV6MzoLCPB%BaEd4*@?aH@FwDrQY@;(_hZ&V!Y|owA~g<=?U~g`W0X= zb1rrjfMxoC5`hS&XYGIVuZ%CnQ2GwWwaf!_4C7FI(QKb&FgTtb!ccf+(ajjc^J8dR z7`+xK8Wwm-8=wyhyf91XX#SRtLV7BHMQs3mA&*eBoA#J{D{}>HlA9Jc%P`^W2zo|? za}vG0XwjVc^K+<|*lrdFsHd5cQ~~WD`5Lqbty{LC9ZSPW(`vx9LdmisXPUp*Ji|c! zE&LqUMtvms7^I*c5x9ANpi=n5^Gm7ox$7-{Q)8I56dBD~mw}3=e$bk>4N}jl4b?lT z<0`X4B9*T|XSh+DWuIdIP}fVDLGe^?Nsi|($_25_{1QqlKhiRca)8Ms_fly+2xJ?z zzUxTqM`~i{hw7!&rTRAo->K%>_vzD=XByMkLCQ(h>c9^ay+ZHFqQpt>&o3a~5}dQF zp;R#YNGGVK!#fZ=DK7`ft@V_%{ee||l(C+J1yYK-t0a9Rg=+W{TS=+aoeA7V@z<>N ztRx>%_{`5Gw+m-10g5$qHzh;Q@`lI_ zY??0;+CzS&xeKgdU)LPshayg|mhj{#%F;4kCdR~}l&io>O$S+*NJt_{d=lIZ&lHV7 zOPbFM6|fH*|0j?ln(_|uHORZE2;K;K<1F#u5$53PJZ=_l-qK3WSA3F#ffGg;HCw>E zPT4?67kQ#OVSYke^zo(>f;pI*8%_Cc*mJqxc~Q7CDQCE4_zTe&IU|GvtDQL>#Ga+K z>=PuM!*$kg@=h}h<0tih_=SQYq8vJ(KTHa5Jj2^TI$E)UdygES8_TsJA4&=0#89%M ztJqq~KmO<0F4T2P3t8K#C+1o+@6fu(IM1IO7I+`f-pV>nd%l{KP|F1f{gqvd8nm|5yO ziFX*ARjH9Q#!|%*KMcT^m%ES9Z%bfv(X=;wy~Q$`7vlty$EFV~X?JJc?enQcvO;=% zN;;UBF6XRyjGLXdiE>7Yeo^E)U|O5tmjoWRij#ofxsElnHD6rpX;*Tu=Spw{pXX*OiUcS2GNn|A$I;)uZ6x(VN- zewbKr$B9c3TeZ2(acv9K)K+j!kn*2)e$ib;Fi5z*Ox6jJCv283gz3WV#8(kmU!Le0 zD%kavh=|@{ix*Vm=9w@#JhBB`pvHlox0)y~LKM|&6p^sNA}84hd^&SN>VsUH;3?jM zIv2J{M8>4~HVGxz;l*}B2i!~Blf0iqZ<9}KE@dl>rM!UTwwNm7P@AgO%cjw%3lpR< znA;g!*|)(S;U_(fssh3g0&zH0?#M7PDO`2Ucc?B;V1lRuc2um))X&{jna!ME8; zc8hSf5-x2Z!V3WLFVftMB9Vc#W381incNyWOBbP7`a1HHDFchcc|Ft$yL^t3CN|y9 zJWkg@AoBS%anow)Cff0hTO`@EANhAgAL#z+Cxo5!rg*g=hdvUjMtZj>fIlI^vb~CJT=3BE`Mhsv8?Uee8k{Tw(mxTir%_5f2Ebo{wT2PgC zoBx%6AnpopFFz=B4tFbW(+W1{UtZH<5B5v$U%M;J2kaTMKA;Qu7i2DhEA;i>#mnS7 z%J&P;$^OY(BcMu;q_y$WCCB4%JQwlJkUiXaqTeglbC!tOT>r9H3#ROzGXuEEW;cPw zz?+Ud@m~Fc`a%&|d!^i87^&gs9^^k%2c%i>wkog2t>j7+st_E9C2w2d%;rk}bA7<# ziQd_LW@vc}&8_I08TRdi;<-Izbw`Bnx}KHI2zDF&a;y0b25jmKFGhEEwh6OLyDlV| zTUXy&a>^LhWR<)~yUm$_Sb-MG=s~c`=X8mGGUaGr@x6$hr-%+dpwY&*q?PV(qz!ID*r6Sxv z)~kdN;#Ct9&O540^RZ4HVpr8a`gW9Akx^TUj@b~TZo`x%%9H?>9Cb}TiW>>YmAT=M zdHyGPK)C5Fk*pv-x5f)XDTOAboMp7Ori+Gfyh-IZofH0g;Tw$|p=Z5TwVGIzIA2jf znj3|Z0i;U-j*{+|R&1(}k=W`rgJc z?Gx7gjXmm@EZ2eyDs$F?%ppZI^T)akG8pr9J zIdZ)?TGY8ZO*kd8^_=DK2)WK*_!j&Q8ws1i!J0l~kb(62YBf%iQ2t!0R;TBu$@i*~ zGHRu-mA(myl64BVNE=bNJaDzM&_!14iQo@O`kY_$@`TMcBdix3?(FGy0GjKLs!kaZ zcd6-_DF;7~j^IQ8zyhn2@;+n`?z19DXz@RemEaQ91mpMP>?i2lIvzG2t}3^5u) zVPU=uT@=E@jiWh$d^vAGO-D~#5+%RzpJx;1Q65(99x4Ty7l-!C!27ahsRhu+WMvl} z_CAKy>4e~fROxo1mixA9_MmsU8P&nq_<3)YS8$gt?}#O&*@MA3M*Us2WMCTpt4Q5v zfh^qct{a2WCNDD1M<0n_8fdLwF39nEV7&V~H0P0eLa@i$Tjnl^G*Jlu0h;Ru zM{K3mW#U1d4@L75c0uk(iDK!m4$QqdA{Mcb`d z;B{ZUUJiHiQSOm0u;xo7qPeEC1RH^EwWC8YgRpd@|A+o)9;vrm_cP75dxg#?7U*1{ zZ3};=`&V-=V7um}#@_3O>Z{7jDO~YU_RxBj_@yYsG(nI8T&wjS+}T%Nn$Ta>J3lYD zXQume>e;TXUH4*342sS-;iq)iPKSVDO@lth>!PYzljW48C{pBE9~K*1%S>DOPl5Lp zOSZ(e1Qxv8@Usn<(ZB9O`@V#LsbdhwsF77$VdTYT<3|yX7W9rbqC;F0hqqx9?0g4` z2)1TV^jd1EQIpy1&E=mrIf1PUc1(YR+{ys2c@5hU|8?>wVs#WS@eQ>$_|MoZ%(eyF zhr4i_UB!dP2}(OzUlns%-c@+ngZlzZv0RZY}2@$utKnlj3AL`5eCpB`Mt=veTu--ijA7u{>hV%y6) zrJQ67w#t@!*+_6U65`feW=`cNtTtu-Ne^2Y&Vt6z8mnP_j(jC3#vF@~55;kYA@Yn!$-sboc5rv^Wx4sndEN8nmh;{l`lVlI4{H91 zKUhx4S_GF&a)hUVh?3*0E^0sK*-f}=1Jja6t2D&86+=q(!^rmjGip{ar`Jwhw9u)m zLe=VKu0N_MoPAW|D`i<;lI|D0H=zitfYOqVRW{v~xyQyEx)!9(8ky*Hj|&>SW2lYH z?q8(85{&3U>i%2k(79Rb?B=3(RfW!eqQ=QOEWb(0*{7c3)25~#K=)Jc;0 zW($y42W{+8M+W4iekY^}K2HF^V8~XI4+4y)cMPGbu$;;+%u+l&^9S|~aalAMpG7+D zb01G2Z?#RLu&Hx4>}&ppem-))jf^eV{%pUGJH@wv_z?;TcVN}TjqMr)gKSl?2_>TB zWhP*nsf4Hx*poDZj~08C_Q|$|Ol8cQ-qM&$c{1$OVnJ1CAZ=5$G+<3f5Zwm98KR;e z0(HV3Fo@;%k+Ybt8EVu|=Bg+h2F`N#Da2HHnxe1jd=<@MK z1qtTO3M0DnCRX=U@6o~fkadqW&lGWuAJt6Oua;Y?}vFg%%6)zR$7&o8-8!-cN*%zv9t8~&n)+a~n$ zT7o)2+K5s&$U}8&8Ut>vTo<_*(W40TaYBsCUeCHhuoA6U6=`!qaxWAHpYf6zj>zM;4sAo+Fjm3SQtv}fBu#FvV*!j3k zkPodv1WWjIse;&u%1+BAjbXM$_>$M)?s*+1erTjW_ z1q{M!M14XWKp((}P|sUTaSoW7l0JMF?q{l;XhOIaK0@*)k-X%jWO9==kM@>IUHKBc z3^m^41ogng%iLh!uooFG5GV2NC>}~dxYzUplSL|CaS;2Bye{Pdo=5o{zJ#!WHp{Dy zu#u*>-a~!EC?Ed`nkF}ORdigSBuF%nhg5fZFYGS$I}(IALp#%Ch1x*xFaCuA7>y}8 zxB#Yo*b!qI!=P6fzMOf=`W@Alb$!ej;m5Hx>;&E8M2Wq@Zk#4s6?8dAi70>se7YR`@g8j+i=x>6GFgNUS{s*sl*jWCM%@|q3YafnjTA@Ct z{oQh3H6d69s#fwS3p&;+5U@tbcX@L?1HMdFQ}_~@Bdtl=gLamHL$6|L#5yk*4Aj^H z`GfRRkT!U;(W}Q-bFz6?R~x^pEyb{k9M-;4XWZF_m}&Ft&cMpls|yq+w zD6|i)k=J`UqqoTx+NP0c!kYu@8azi}>h(>hhC+?;cTN58NClvCy=x#R!O`95x?rfd zGp^tse52ky@n57yJ3kbLy01oiIiWojcZ}zQojAOIb$!+9ld3&Uwk!VtJ6cwbM-gQp zkC9f0Z--#eP&)#-(|5iA51-ZJo4672rE_&?Dk@J;@|uI{S1+*LOxz+?^_{Idv?)>f zr4cuM3@~r8TT@GT*Ct*y10HPuGCo)v2`L)MD)52{2InLa5Tw3^p$m}byABu$OEsTt zZxfN?HN(&G*Xoz)dI_Q?kqY!c59<db9N*oOZiYW($zwH;-VJ`h&_QoBm$6E4GoOQ74?Rk{fq#l{fm#t~p>pfykpeK- zf<|&aj*;+^(n0_&>7>Sy?A>jsEu_zucbF;E=lze6E0H|abF>*c2snWOW6lv#*h@GJ z_!>R{|F_nafFjEB)x=$7vxExLTMB4NGWi-+>-LlUj^=B%gptWm_Ibd=iI0`Vh?Asy z+$K~r`34~d{fTm=;{+B(Jzf)yyF=TScZZNi?~1=j>|?Yq{yPucgXH)xMWq@=J#8i>a9EARqw_;ww@5_X3Z;@N2 zTPx3?sgjpDudoXx3*y>vdU5+=Z@fZu((NPuw5ZyeL0!RrVTb^o>+~1@=y<5##2A3Q z(j{RmVUIMY+ky}$)!@oN)PQPM4j5Ca*cE4jJtgM^Kf@iCaos-S=Sici4^z$w!TQLy z`-4HEnf96fSo(U1V{ZmJ8s^-c-AaJ}?X0fQA#dsN+3(Q?ZC~sfth4%A@K&6iO6v9* z$CU4~{zGXJL3PX9yjKQ`yxP;pcGI4M8%Hux94K_~NXspFZGV2nTx4|5-E2APa~D6h z6+~G~|n{bM`NUB}Y%9{2U)`M2B381BSoSO7UzJuJESkTf4yEy7o zz8)a&Lw?{&^scAtP#CP>g3J zyhRloL@U%DAXFeILLspNwTu=@Dn`FUPLh+c1I@Q6zWAiFM^qESova1ao20g76SM*H zxxjRK2-V7kPv=pGEq1YxjKi7**ck#z;DHY%c2PbP%19d#T|_APaML>yhH|lN0U1TT zn<=BfXjhgcQ;X?a1D&Yv7(|zDS{~!Lr6W_%dZQ9z+!!^ybyyJN0eJ&%C9@6AF!sCO zYy3qtVbzwFlRQ}0GhUFx+38EqQUZ;ek2fi2*`Hk|sSzAkOBCZY=Rc)6ipl!{}kGH#oPqgE$&qyPM^Lm$-lS6pCfG+YG9@%9ZC4tAbJi~AX z-pQwt_Od?i98|aT8?hSWDlLN|u>#3-Lj>-YWX_5Ug!y86`X*wb=yA+zQn9Eepo#QS z_}S$k87^d7dC@8SPMI-}#X}+}I`Har{M%YfL=hifWbrHzG)>b~!A;KhRy`zlxBhKWijJ zD*v_mNWCZAEL+XBsyoS<&$-*=NNDC9ZCwT_=bUU0s{6ya4KXREbH2dNr?rob{YF2JX0`5DD4Vea8u=c}#*Is3tBU6f) z>;g11HH)poYNOKGH}NdrEY5sl;GE|iEh)h41D8d+A&#Q6;Z(*9{XMc2YhbjZ=eOTw zT*ln2xz0?-$qFZ#8wl|!D_O3@i;<651ahu#C~G%G>EywVq86JsbHq$f;RR|A-kW}y zdWX=A;nFa~F3@YbIq6`H4P8fmQSgW1Np(mGWUQjikL+an(VczgF&Xq1PC3jO2FCm# zdjs1;fG0y~!!#zvgI{T(p~B3Y>HT=x>3~7Hn29a2Ys4KuV0K6%iypg7hL)iK|+^k~2gvDyb9~ z5jXD|<+^Bo5{ar3?u>xb{Dm!x6f~r8sZ%H2U9i>S98=0~;EoyFZ5X6KgesL15lyU7 z4z(;HRV&t4oFT`{kL6`iY-Nv=OeiO1mJu%0CTZrPM(Tj%qSF|yRlLA*4%0+{;;hH9 zyY`Vh@w`qv96}HqJe!$Bjc#j2EQzZv$-O{sRDVkxp?Ik#!cS1vDVd8xDE}ywPLtHb z(j3cthDu;ypTq7Nnn$d}of&9=?Zw~e(>J{$yzJRgevtU2>z`Z<>0GBn;tH}tUlJ~) zxMnMK|kqk@kz`HF?4h37M^h z%@m<~dt~`z!7+$M?g9Y|c0Qp=P>HMv-65zz5BZo0RM?)`NWl<3$uyFGh+58m%%5p1 zBG&MA?H^$>emaEERKfRv9WHyw`+~H|$>v=~7sMan9mckXw((Bl3%&pFz7W37-om#h zwVAHtt)x$~Dgh;ICH@W2gjfsB1=3Jmjk!P)rnYnjh{w5QOMp21bz|Z|CXpOE52z#k z@cspGDY>&h0c)sSGY?>jxt;NnBgWpx?cj9an;@y22Ew6+HV&Qmr*t{TKu*Yd##v9H z#U13_p!S8_iJK8MHObc^n%Io0-BP zb_6l0obz>R<~gqYid8IU?uyJ_mNB0p_AslTdp2Y#>n>pKeTel9sBkd0-5Aq`mvfc_ zPpMq`Uf~RyO#e@KuRV=XF5FamoUvbsFaE`J6&mkLnQcO|*mCA}!I>qmnZE@qy(d_+ z1lt^jSv*0MxtOix=TjiGQH9aNP5Yv#1R29=6c_bi`yBovg`~MLoe-L{)zEc z@^#57W|Rc$EigJAQXRH4kBQpMPqWSV8ZwT0LVp@LO`ETK+_sA*)IP4E(=9ani=NQw z>i&#C`ePMu`4A&TiCfaapexkgO$@Yr(BUX^p`^^h+?bKFgVaYg>pOz@LIw9y+U8Sl z^f*@kpw)MAifFVuh7aji=%sq}@>=>j?aw8l^nWx*yc6k1RcMC`j2`KGi)hwj{te<2 zO4;Z|L<8mY@Xyv^YVOe7YBlx3fJc!pt+sDbx|(*oCvo`#`qD1+;=lA}L#_8hdc0<% z!wtq_xs62)^A7(5IaT?p?lv-Av8QQ%n}d8q>+)*3>_)qD;eMGT_*vRPX)SES(#?{a z$cUh=k{&e9LoYdjZJGs$58=0)I10DZGKml5%UXXU++{iKovrp#I%G~&kK`T=NqD{J~9+=V|;aY0x{^vZ7$yd~jN&IlCbQ!zDye9GUTc>;gh0*`Y7U%J0t zGhe~@Vp_@D$bN?N5C)K^AnOF(lyyyJ0$=KI`3(OijhQ#im(uG};`oV-gy{Etd!|X? z1KvmG9*+yWCoG~}FAvGSXSxlz$z@<8`C3*n_#l5V>uTd7-VJtfc_MF=y(7<-2jVPB z-oo?aXrtvk6VAuLao`oV+~X*4j(gSaEPw#mW=?jc#Nhq+9kg?}qK6$s^Dk6sG6@Gk^nj2@DE9(%aA1YveRxElUGvoH;9e`0%g<= zTkaE?Q~i4G7U@LkU+$oEan4^ZOR_oXF1JRK5%q$bExs9;$c+~3Jk}apGuZZSoD0Go zW~(@tcxO;`+-%LS_I$3brlj7Q^N(6nO5>bSZOYMcb}6qU2{{vr|Dwh@Iz?2V6^AG{ z#t3tQWIX$Dj=z{{cArzp`-tr2u)F&}Fgv?vcDPpUwWjpM&_qK08zha%XQII8bNH?(bh*vW_k7 zm1VzStGeGNnX?UD?NRY;!0;hpJG)4C*aO0Pq@HbG#152}n#Zt*`Ad-VIy39OwLQ_V zZnCMR>K3<}uh^!!0lJelr8a;!B%oAK7$d?-sX+Yl-Jx_uN4qXmCSV<{zsMMbW)ovW z4}BZLS+}uuqV=yfwLPF_o%%DlyZDpp4)jCjjB+0$FaCjI4Qe=ihx`uapzol(9=G1L zQVt@N+mso79e5*w%`Ai6(WsztEl^0oe6K0 z6%i!9Jn4CouPaZwi!8I55YuUZkr2u{0NtY!qxUucqin{eSDjF-!0j)pmNyeZGDc)X z;$YknsgCRr_E++V((DV8K&V}=Q<5aw7Mr)CBxadO_5W`~H+L0Oa^fKS6o*L}O<(0( z$;FjlWqT==g`?6Z)QSvC>1 z`i@0>UZ&R5^$?&}w#{JLI16I z+eHIov(FoTL60Qy^jL&jjI;JL3K_gwa~Y%c&eM3~1aoevPZP2&f6MbI#(9JgV9{FQ zdSW2^s}FV6!cMI4Fiav2X9wvcP^%LE)$YP5qStClaQI-9dKn?nJ6A;|K5=qYS&%VS zmC}FcWk$jh_MzsYt_0Nksuu<>Mp0a)dxi7LVrwP%o6!cAjr*HF4lCNZJc4Eu^GEZB*7n3=E9J{~^~4uS5mO(83!*Dk)0%&^t;Z6L~wm z6!!}^S+5oB6Kpm)&N~K(>W`~;%FmWxROQG!3pOdf$cr*c6>Um;%0qWU@h`C`Hzqet zjK@j8s0$*GO5UpW1?7uhs^YwzMXwbfoc;(e$-JyT@$sT&({SJ>aIS7zW!8PX?5W~F z*P;B+a#81@^aHX6!>RZbX^sA2*+uF*d6Hr~0bvCif~fB9~kS-hVjtZ4^YoVy6&wAIx4~F`7A4WO* zQo1rR>2B9Mt8j_-PxVZK*8G#QnJP3A1Z-N>_(&2Yp}2SOIV>d`-G2ZPn>?rY4$60_ zwEGQacF5x{2kbB37J~+V#qG0RK~&6+)Q*y;Emp`sGG-eIL7cJ5H$w#|@1lYucO7NfEatX!ieZ*V6o zg9ldOzZO;Yv4}}ojXf+9J!whTR`SZ2%Z3+}Ga(>-5cP|1sCG5o-Q7>aW+cqsslqeA zS$vi9IgLhw4lt?E_D50&3UBs$&=i^9yBE{ZiMKm(jKr93{buH@kTmTt=40Q*8WL-d zdyYDWJu!Pm>B5<@%#+}ObR)qy@vJcEea2o^2^gpoW;MX9U}PeN;HXA9tE^wh-q4mR%IQuskc;EhsTD<^KXq%e}f!N*fFG zoyVn(8PD}^q=ktVx_s$^=wF&?srixz>bX)=Uq@r>jh#DR;UwAOAeEjJ&$9e2{42CG zN#v7&;IhfC^J=q#FvC67mJGe#PF0<7Obb`Oj6S2jrPMAts47&}`T*dYuwLGZ1Cyt1&)$Aun;= zvK==ZqokTql-C)%jD!F#reJ1rArhGhn%IH5lz3#U98Pk+wRMx<@GawjcDf==o;n6gt)xqaKX&>jK0|CGKs;?{RN}RU&g_FEJ+O!4Q(f z2#px9C-3$r^zNq2c>V6)NXvKL*=0r_pOvGF*&_k#B)RLbpzsb ze|%@5*f1ZWcM-puyHaZ|PPT(8MnoOy?=17d?o?a_mBKU zj91St`B=!oF0Fjrzs&%VUzv~6g~;pX9@W@MSJ@flyT#KMY;mZ-)<_sL5|BfM+P?Ib zfe6jxgyg8I5_^Kn|V%4_ajb%wmtZin1N^2TCJv|eD$ z?dMAYlPOvoqUv$qjdpF_q#_(V*p$y=LwT)R(5v9+_S?-HkY$jUCCkv^@bjs`n5QTp zJOQ@^^U%u)myFA{oXg zSEM7IF_S5+sIR!4VXHC41cB!%41y@I&LB^a`zGGiX<)0mE;J4xHb~aAkWj1W^tL}3 z4${e3-WcB~hTOxyCjUn~Wrp9hp3-P*6+q#Q{ zTjG$$XQVl_Wi8X>X!s1Mm6F~t*zuQ|S2PAi)8dn7!=-em(DjIq^uwOT$PR|7br*4( z*)eKc@tlFtBWh5LZ^HfcdzeJZ&!(-+r?7Xe-K@g;J?(9*^@Xp&AK0cz0+=^DH?#(> zVADKP5s7S+^((?d_K#s`*-nAC_FWZ|e@_6Zo#Bs?&o+ATQP8(73;EG?FF7k~Vm+6(f z`09B&3*v)1yoL|SZCtN5uhq4jQHl%FK)s6jiESM}WM@OJLWI&uPXYA4WQ(m2-e2h0 zzp~hB5U&!J1@?CUHI;vQ%ZYPp;ob4z-iEE6{K2vFyRoB|(I zZu1<2=E?hQp}0LFe&3NI*Oix*J4$26qqzN*t|Ll9dyQ)F5!kZ+QGa|*SF>|ZPrkPG z-!9ig({_^nNXSvJr}l{FUdTC>rR@Z+T`cS~Eo@vLtF$Yjt$D;XtEign!QZUDHPO_e zu3tLpR}FaYd9bAs^NFgw5lDtblBZG zynu~>G|eV!k^i*PpmcOc`&eBn<_%;^K?9Bs?@YLWe~Zdk(m*uB^tr7fVz6&55%e&k z<>2o2@a7|$3UEr>O5QnWLI;-Q2zP_JLP8O*;F{WC)CT0wd@2TyNluuLn~kem{1hKe z2zBG&_Y(arx6;fhZvC2;{ZO3>28x950em|+$kT*rNGEz@$6weOmRS>u7{w>&O`!~g zXYnjd8410Zf#s9;y4B+nDUFt2DZgnmy&D_%;g%})weaz)IMblHg#Ylkj%w1f_K%Qm za$|KK>fRKzWpm!ZNnQ>J(=rZP>+zdFH6&Z&`*0OqoucJCyFWpkmr&!;tB;-8yhc0HV zGw@YJ0&gnUf?oo+a;9L*fFp5Mh)`fEcoK;M)NW0v zI{?Az7pWfz>YQC&E?XcOs#_yfGp!pBNz`Z-UbPjJ&EmrEtj4R2Elof$8H8>v-p|y2BKK7PPeL}w=YG+sPXO@rU%uxcD+P( zH>GvXX*u8Oq)#t@1v;apX4isq)kk8(q3x=O;40Wo#S^#9h~u(;>#sylk&Sjw`OsLo zaI`vN#E;fq#~yry{Llz9=B#G6Wb_7>b3kTYSF>k#$PI(B7Ld(4LvS(7L37saEFxEN z$0m$$)ab(guk4>SIKjs%{G^t8toF#nG!oXZWAteAwdS6o`{luH%>!mxm)bw{7A=1a zS=4nSI2S6?|8jc>f23M%(?DQ}R(E-#qpP&iOiWW<39}l@Z+wEuzzwu6ZT*2i&|X&| zAUuQ^O(7&#c){`{atiW8U=d{zCeg*5>V>76-(;eQLk0^tt~p%HL|U|Y(odn}?axuy z(Z3Ah53_-Ca_Jy0mU=y) z1HX*6)5Vl9D%bLjwKI~5XSb5 z7IZRm#nN!hE#{4YaO`bXu8SLfH4A0AjA~%NRQfgVTWuT>VeTJ>kDA#IQd?NqQCHhtMsCjhYZ_@_&II6M$UWunz@3mVYUQ zypyuA2AZ16nb+K=QWGF;bCul?W;<0OsV@Q_lM9Mxp#RFmX>SpxQbSAva))H6{{_?z ziGvFdyF~QGs)T%2P$V6yFYOLxFKNo^yn^p+wJ@}TkAY~qvvu1#u4prgMNnt;k+g1j zpsFm!67fW7?tc*ZLSf-Df>BE;R(f)?5HHzW2O4_CYHDm4w8uSZ@$L5jhqR6Nn$<1q zu<3w4_?}h*L+b6L?;@hL<^J1{3F>t&yD_unDy!3EOJTS8N$u#WmCVfz0~0~G zie}j8Xve13&%-vg7u%Z$SVdRC8+!kw&4XU;9**7(AL|tQPa`(y`drSVCzT0S-$)09 z+Z3imaTSE~gH%!HOSnUx+qeNTKt9~^q280iX?H9}Q&S-eQ~#kk!oEgvX?Ktc-#|JG zeQXZEIF5BPYiDyvxiVkuv!*rdVthbr7rvfQ(B2AOPRs@Ws=ZIjgt3Z1mtm&S zNZek~Tg+{OPxWhD9FbCR1TQ0EE*=QqPfj^Ns-e=&HETk<_Td6 zw2$0QdjlV!bf7s%JhiOt8G06NVYMwrMSERfgZ)e2l$?d{U_c@+5PF!i7BPth%q}NK zGKTrp{11H-`yu}(cqQu|btUu~I}cS4zs6qG`U0t8zpjczM{>gQt1gOH7|cEQJ1J4BLTvXX@SDQL~x zhaMF8B;{d!_zxn&aE<)6i?Z<+e4SG}ae`lGagYY)i2*Q(tvEvFcDz4eV#VEyB5^L_2qz)oz3``{9rX$SD;L&QV^~NU zX}_Ykf$xTRXm2-9!rU~HibD9m>a^U8$fGK=#2$3L;#&A#%!vH#A|GtJe74glp+Zt@ zSwtlZx;R~}E&W>J6VS`v8TdSKVozDKH#DP5P<|I4V>psaN6ykaB-Ws&w775``jf_L z(Hx9cg>c%5zbZ?%1gKtuUFYZ-z%J46HHR->@_EpuMW z?ur;D5_By$pLrVmI04N{fNcof&f12I^?uI|LpRT^iLjfBpuYm`Ld-?(Aw zJaTEsTIvmo!22*QpDJ~*W;oLTGYm_?vS##PP7{yfJaMU{*I+7MOa4{gP1rzrSwbZ4 zqF%`gCvBtcj`Jf=(Ul=>6gs2Wdkf_i{)N zf3vLW*5VUb#1(4^Ev);QG9sQ`5IaR8ve$>Wk#jkA-ecr-9Fzl=`iE<79?!JlUZBlE zF6Wz|Z=!ziZQE%WE#I>CAM6L-`{EgVDDQU0zk~|jsn`@^D{s$|o1{42E^h;=i+9~& zfEvs@Vyt>MKFhh6udCh{1*KMzn?N6wnH0;@rd?nPP*S~rn?4|`FYyc~-Arhc=e0Do3>)f-H3RQfr5CRa;6Eb{4I{O88(si(tG z#68%XL2>H~#DV^y>Vv4<-pQhL^x5vC>7&@B&hN|PaWF&9k`R27ZrZyZ&s6t0T97`- zBo+kP6@eMm6u4JakD35h*Li{b0D9v{4H~F!IZ=cJia>YLV}Ua8!(}!A6*dv{#h7BU z&|@zz2^~92!|THeO{{nuC~0IOTiSRQnZw@Qa-;PP`x$6!buPySTu=z%#6Yj5HE}u+ zRZG#FNz|I43C>T<8IQxN@m_i z+$a#SmZNT`PO#LN8!<0g?{PbVTG*uooX1J_TH*`41)Ndxag+C4F=I19O7lmC!uHaR zpnaNm(W@}eD%9bc7+g)Q8alV+H_ZfICkq0C+nv$3Rsob921W0qQ zPy&JBWGdx8usgbuss?Tbs%Z0h<{npRCA?~TYX*^rH-oeAyf#cQ-a)*%;|gJ^SlW-uF08-YoxQkEV4=qRbaCUHJ2nk1$dF&p=PG>^?@_CfwSdpQT~=b={RYrG%ZG zN0Lg3yAAnKLXuGTHgFc%R$K3Ji0r8fu@}%nq!ja9#z}rGI#6=FO3<#CEUsHvcT22o zRF*st{nxTKyH_Lw?Md_&EdlS37!>}6(ftBMa}jS{NuoZ~XPddg0bH>07R8kM40%h~ z-^d1Gg{3X*+N(kfQ00o3g6$pevknOGP<8@R5Rb5o@D=!>8hyVC;xTO3Zov>P!v-d> zBpUB&dE@lm2p;cddqa6~>?AwPR|+ck;lvOy5&H zA;HFVEAJ9fY_p9wM5!_oikUU=C@vMTsMVhNjJxYrA9p`=`>s`+Sk(B*5-+?xIi%BuSWBq`6v1@QoTSC|pLQB}|wdMHKd3 z(!-1m>=}wh+)>VaYGAk(r}tlS;AL1Y zsH6dMmx3JH8<`~S z1-(}KHx@!aC>4cyGCZUnzOIZ4$)kC`j53M)tl2Cp;VaW8>;ztJ`)dkV=i9)dI%);w zcc`P9WBD&>_Uh|t!!)VtQ*0F7OtmodE4@wG=4(RlQXHBWK-bHLXN593;x;oE)-4{a zy_nqFBdh;IDeXpBXI5Q|vLHMA1L^PNzencf?SuZsBsOgOR{sky@R^ z-{A9Wwu=FzYl)}W8ucY-kw}i2N_ru3#?{40g)0d*i*tqb#2%kuVG`NO>76j1QfYOc z|BL>Qkr2$@-{L2{ja60O7YgwXD;k9%1X?ynaF%#5sZD?;Tg3PZTqwDVZTWYp1fTi* z!!)_G2Y)?%$U2yJlVw~R1Kx4IH#Z3^$mgpA_*W=5ig)sRs28$+@JndZiQo9!u&j<)M|uiHchn+HDmrtrY5);RZU}z)ZRC+ z8{U>))Vnv|&y#5{v`weJ)vWHwiFH%UAa}z(Q~(^|zgT%3dDbIENkzY(eOMvDMOx%b zK9hZn1S^(L&2#MwP*BMS&7O|2+!N{n$iJy2Dh=Fsd7n~+%nRG67(}D|e#u{Bi5|If zExyFTR3;OMooPlaDWjCgM1z0hLv(KT(cjLQKqGZnq z@0V?sxsr0ioTVk?VZRBSgVcDcWf0L-7atu%Av`#pw3Hiyks{4%>wf*rt{Wfd}We@wvDS zm{d;5k{I)fW2FG&L$*kAnmL_}len`omn{^-SocG>iT1JuetcuQQMiXlxR&$L;f-J$ z_p9Y)ek@?zrUbSa3D%N#+&e{ECGkLAwwG8A>`(R)Jpq0#eIm->C5Nho6FiL{S?J7r z@6jpH@EMM7{3-sBl|7HZ#~BG)<9t&oJ}2raN)o$??q_Wg!Nn#?mxV_}@0LyrBSnux zQGyB4Yd@I4Rc!7t%9n{F9FurkMUGaD-N z<)M9oR-1(=x`_2gO-?cQm6Q(%8-{al_o9@PdZIdJr7F9+ARV#Oo!&5g zM5lfY;!seXt_bzkCrg`*wQxz$OySYo z<}&eow$TT|TL738k@e3AN3$_HfE1Pzs$D`RE!V4GQzYRhRGX1W_%1EQ6ib! z0xv6aSR$VSc_h2t<+Ci4(`a{Be3-k(B2*Xx>@gA!7zsSBC40Nkt7g1!N!F!>QdBFy)UJaf1A3gKaGX*i5&HR)7gNgIutyv90|9kP<$(2|lezNBd;cHzkH z)}DxMLx0-Cf&&M>fln-?_uIjm-2UzDL|k|H)BPF^wg&6Cc&f>7#RY1Xkx<3{leKy5 zSetF~(UF7gDa*GH9Riag$OEUL8-lI-F2P?dr1yM8`MY~}<1iJD>7DyB2ScZb5=Wu;3cp-5D4d#@*fBg~2Vv z-QC^Y-H4sIyC)>M!#Urrx<4+vsA8t5r+W9ShVH%Adf$x3IXj53#JE}K$oWy%XFjD) z4MC0i&?bE2hkF?}U3r72SdZ+idJgi8=F1I!qFxh0DK{&bId6z;nfq|g4$ATjhgr92 z`3XN}{-z&`+B+J>tP8;oqgms=fWZqKitEAtU%VN1;O=Ol-u$obwZv45s7Nyr0_WUj zfO1D?+cQ_B-yh3ly-2t}isb}H?Hr!Z1&4?SAMuv?_6`*Cf4cefL4?D0`??NDvMj*b zJF+;_XYo^RRy;ntfZLsm8q@KV>AOcy@tFxHM=S-vsI5a4!kUo&fe}%Y@BY4LVzL{e z=bPk~y{n;1rmBB9;9a|=39K!L@K_T2=h$@%FGe@)|{^+*A(^Xox{G0D+z`{ zs4^>RcKEwRXx@&a?cdxD{Eydn+n$zT1B30Lv)h2=| zs*84w#2RCAxI=S{cIh?)f4Y7pH1^jR9iybZJB(Q&D|%v#ZGO2%N*ByM&%n@=9c(oJ zXnQSD90Szj)}t;|o+RwU zna`k;W)i-{%pjj5>wL~q6DdEf3wfc8_&M>w8z4i^Am|k2xa>V-2>y^+3=c(}L}QS% zu{~{A^aP=-;s&mUbT(@vp@|w2y@}XQ5BC`&GZ@*{BJMtR#VqrdMW`X;I-mpAMcN3; z#FOa{AWp>Ds4Une^4eBBQbyfeVTrM&4`f;6l9(0IS$Gy}pAU@qoGrB8z{T(&qhA^w z$!NpRmi^Qy@e5!I?L2K8sESdA%!0Hs*8}?Dm2CU+TI2+$B9o5EEaMNV zTR0HahdwE?@hQNQMbm9^IXv-!q4m`nl6gAE`eDgEA)tAt)R!V@^OBaqK^-zFp~VG! zMye~D20JL7pRpDhC*2SwLb0X0d?Yxzbcf9$_J}NEu%zOL=8fiTtx=;86g2MFxRHAS z81)!z5cowM*i7$4sTP!OhO$)CGBEIu${kTeWRpeX&NQV66@cRnK*c3=BX6dh4+(p7kcwb3w5 zl>I5ixf$HIwcBR<5ao?qX5NOp1n5WBHkFv1 zsnn(Box%ZeMlv*^Z(-CH*!Et}DR}gwt^;(*g~pz zF%QqD+ob*?A{ZAUR+0FuX3u=;4YsQlh`*F~qPMCoo|LBa=)h7uIQzh0>P12p^bH*X zCL#2U8x1V9H>c1p;0d&%=d)E{k7*mO)NQQtNHtt0la`9jGeFwv>|?n&fpvAXY04 z@a!kCq*;@zTjprESC3H*Ni&-}RT3t$^{=WCTig++vgmjTu2gQVW5KGFxWYQ5nKCfN zh)!4BiSWX-DAszOAudwPwSLC6k$=~3snr_=lJv&KUFR4o0HMnXqX6zU#B|hxzUXUe zC!lURM!_HWZSB$&EGk4}8Uwmk1NX8cyihN(DdCWm^K=`kKMduG>l#u91L(V3{QB>p zZ@0<%=LsoF5d zLDW-kGpmE{)@(Mj58c$t8=2n@Yrj6Ut+pNPHE^R~3CyD3D+P!k^hzT-kb8T=yj1vo zhGCnFY_WQdwy<)?;)}vBbzTc;G+mSLd~4JLz`QxV?G7easPNjL&bHBG1!`#Xa8C*V zeqwMaf`OdgKf`MyzR`%VwPa6I?>7eHOPk!K7YKV>eVBaGwD$K{cd`Ji?ch?b!@k#U zq9vj_3WDg1vC^c|j6VcKsGRwjbi>V?bBt190TeqisRnD*+m4wMH_QX@Rr+t-PS{)Y z9s(A5xxJ3$j+tMxhrAIF%kQLi5roF zf&`)|G$mSx9Y;y927({3052w$Ro@~)DY(3qu z@Dx8zBLjC4{RA&y8Kf`N8HlZv-AFj<0}b3(g$<>@snIte1r}E$BzHA+3#lVgjpuOD`W<+qt5rviSv6(H&7iLS!55bCOY;yUY`S zm<K#mIL_h7R47r01>b7^EM_#UnTkE*1RQn{e8UzcPSzawk^YZb?wxx4I zAf%4nW1pL|!P`e~m%oP%3}4B9gNzw`n^1!;8K?<)kLmAy#vr(@`7zT}b5uUg>P30Yc+Xyljh&vxS&IJ=NaMDW+MKNg z#grg(J820kPPUv_)Dg$7CA|bA@sB8Y7#D)1u0pmoZJ@ougq7W3#Ni&zh+|TS+_;^r z9`fbDa&`vwoO32`CB4mDF5z)4C0DRYgeP+aUV~bSqZ2!^Q@~NAH~7xRB_=0_zS8g1 z*W}$e2~Y8-9U}ycTSz4-V%g3pI|)@I#_HG z(Z%-#vmu{(&!`{ZNBC>dsi=AZw4)Ux5PYjI#OZ~`5@&)$6qp%EsuHb^RgiaylL8$m zJn?E5Pv$cT%Q8Z^NqUcu=)5OOp*(}$lEF{_M6XQVK8m_4>#bXab&)M79>537Hf5Y5 zOvui}c92-Iw}C$7N_nVD3)5Xru+#{h<%7IBkhhjf{sT$U`XC43mYU7L&q$gkyDkX5 zSAC{99{W;_%FyF)svTqViS4R0fl;JN)ozy_MyJZklv`9NZMf+6(VlK{M(3&SYlxT7 zjosEjDk9$KR=W)~)D>Iw0CU^WkO9Ko(Ralr5pcQ}f%!zXPU~`neo)hD6(n#}p5$x> zb`0+&4TI(l=@Br<&|ogm3_i90Y%Lr)*2gSbhW_3gn~{OD@1BT_!Y}N$2y7;t?7HCc zo?fggu>$f}oB9yr+FIwlCi!$MpX~vE2VOar+_n;yJ6cg2fH*xYDPp7Z2hV2sW1$0i zu_3rWeXj$NguEWQs~7E#{=Aij4^g@BZQ1vlQYpclEvCy>a!4Nj=7;c;Ny zh2Q!9$nmrt{HvHpQ6qvr{35?*VGQx9LzQ@lGP$=mW5;lP7-sF4NK$5I=TG=`Rw>N3 z&4cZYcv|Dj{)paE@SSr3$4;H#ZX()7CGwYPknPFuB?lv}#0)?Xq zpQ#^_uVGv|5CZ``VeoJ(tEVsr2u};bS&PU4sYv!3YVx#kPCq@+uZ-)?*y?aiFpIU= z?3Q#;V8i@F48)znA0t~3jzig$4WzX#6Er6ZwW^WcPK(G_Ge+r`Q{FO9FtO9dSSMIF z{R-J2j)S8sAId#qo-cVOa${`5p=lntr35iu4H1#Rj0Y`YyU^T83gvfK zd?D`=l;yFhS%Qg_OxlQW!?a5J3K7_k#K4Ot9G7rQCFQ2!l?01}qE5wi%52#6uH$HbB)6*=Btl;?tZH11y{!LGK88Q2ugoPAhjr_d`^n+D zxM?nw3hgDoO;m!mqSsP4A35WZB}lix$<4Xi?`5cT~VJ0?&&`xZA=WA^uQ${TQp zdyu)ACMRKZGK17?R86xXOS{JXj#Bm;hMdH#h1zDz8Nyy=0vQMC9h0N%;jN?ZJ4TTg zMnDY{=-Wd(%e}Cl2J>^B@jv??CASk-^sSEkOS;|L?{}4wZv5jkpOvdSV2Ku%Dr=}B z5w9s5Ef(!>4F}OgpW4S8tHiG0;<9Vv7T8Emym&S8M&f`Z9CJO~QL+=S^RAZCi92n- z$WBwfPewK82l*>+e!ChK%5UmC(vix44!zwFC}>CQDm^P0N2{}&g&16E!gt|c!u7C2 zqWR=_?@n<6)yJ+}n$DOyN#NU&RBV6PZp2wm6cXKT#&t)(s`uue!sV2_;;9Jo8FTr0 zq3U(`e6SE!v9O%Om5I@%yu?gg( zC0U$iYS#=GZW_HR!H4I`coPQT-C)tY1^hYe^>$}PgS-Z_BH5s5A^s_qPgx9GMted7 zwq-Dq=}&6q%tq#n;+ZTsOOrKdI)0pqKh6Ph{)T06V|jtzV?1ZRm%WdWWqRVhLi$uf z#@!&NvzwsXC>J@JRvfK@J5qCnZs0MB9T_|L)mbsj%YyKDE$f!xYgiO}jqsTFG0qy1 z-X165NRZ~P(hON3_8Ku&_#1MU^hLA;u!YhuPN>PJeiHW=4bgMOUo+1$=n{B*15+cp z8Wz9;Nn5>NvnA43_Urg_Wp~Z-k_GZU%r`t&u>=xK1S(Dge8|p<@#=#Vjp9U6BK5gq zf2M|>t~d}M!2p>$A3d3AN?RWnmRL!!f5CgKbT;2A306d5%5Y}-Ot1w1Rd3N^B<|I1 ztOk`HTQ{UF41gpn98eAo0)$WZ&5U(=U`6t z9_oC9yU;tWWeQ#ka&FcC-{T)rx1rBqgV+<2M6^xI1u0uKZ zgl2(7v>2q|pnjuIkEC`ov3G~3H1Ec5AAC}^k61V`R7fQ~=*MRKr3m}lr{ATX?WKpg z(suVCeZUM<*IEYx7omM+(Jb~<3}QT##Z8FLo62deam@zBm-f?Drk+o5OhJpn2;-y! z6$a#{SfpY%WbSJ0eFkW=^g<`4NhbXURa8!u zen%kkc`_e#QrdD^DfVN`8(AMg8&oZuAl-GB$!Afn*<>iZ8Mpt9NiI?%N`|?0^oR~1 z4m3){iRiitQ&kXldEQR(QT*35S4kkLFs4hQrVxXsNq*39?n|Ys8GbfO*?P9AA4hH` z3P9xW8!=nky9L&`uMKMji-{iPQegn;OJ0C*CgnovsK}GHBqmoh#^?-k6i;Qobw4Wp z%5JndCS~wGPZGqv@KDZvaw~8VcP=%k{y7gpJ5qLoZ^KB*9po=%&Q47dVwRMXs*uvvk1>yY3nIz65g65NZ!KISvuy!dNrpAvt7rsJu1>#{MN&MUQX}$E zndY&T`$N6ME<_llQkh#w>*VXfb(A>c9RQFzW;E6&(yki)OLo)GcL}q9MzZ#f$brFK+7ie`&BM6yNxy7R7~qKVrA z)L&@zs@|i|X&){6sgriT&w8N!37e7Fr!^VnM&@ai7^L50?J3+J=X=^Y#C|JhO(Jd0 zzcFbE+O9bOyxt7g^nfO+o@xrAc16kR^YC|B>1qY)SfYcv9@`!9PF+EW^t-LrkVGy4 zY9i%|6<#%yY3k3Cn+dme^ec_fmL{Td5Mfp2tyG{!3;D`g%&*Kj%0xUUVMG~9+#In+ znM8i+cU4KIesF103K@1*yOar>!~e!)N;_S)7rn7jD|>?dS_zR&5T+Jfll>%lWqQdi zDOL%|axdz;2&O!jzR&N1T*gGXDCEc4iPmQF4}6njuPG*#K%FFkVZYnb?VJ}b`(!yhfpw$of+%^CAU)i6SonuA zt-(XYWoj$JMK@XJ^RJ0byReLUF_7yOze+s9n-P&EzRhp*`z#3-P+Tn~cZIjCmrKt} z#wH1}SFNuFzP#vqvS1D0yL_o|8vji`Lns%l%&-(*6XN3eq6kq!gh_iuG~w?c+9ST@ znlIig4X}PM`6M&-kjhP0YYLAl39h@&_m-?KPvS3;2ImI|9HqSUqXLffR(zA-rmP|& zKsZBo(mz-zmy=zYq74c^n?y;*q*Ci70kDYkQ)OKT;BHm{%8v4?Rm!}JylpB)`T##g z)f=D6U$0sl;VlSMJ@QW#;MGm83xwxX<0hAxA(gLLqHLo)1Q5&Kpr5FH#*ynkmEld| z{HOBmz|wZ)3Af zv_x$%)5!wloz39>^-aR+MSTlf!;1Ix4z@4L?(b1`eoS%iUI$B#0e9OVVZqysn=rLr ze~piDcO2Grd6R-I&^jjlp=lnfg{4jEo;2V<)#Gjg=wz{{aX#ekjPG5S;RljOyJAs; zF*b(9*pA?S0}%h&+t>J9MariJ)fLO(fP*SE-%P|DsO`lZYaX)-=N}V zVD+t-k>qgQRou_$?K&c{BbcI#BhUAa*JV@JI%aBKF!+{UY7+O@Bta6}Fjogg-K=cX zKEvh~9@Cn{$g=Ke9f*RYZJM=YZZuH?q0)nEG;#D6?|e-%)5(#pPG%2Sf>m*Xs!4*h zv;KtoG2vFl67?YIPGPpXn6fpCr20y0OhTx((qBersKiWlaDl3s?dFYA)pHb%JC%!g z7c4g@ZA2?32{LKDr*b)MX@$E|$0#m1p@cAZWqB#5vA!flDt>a(qJ0&YP5Q%SiVeKo z-eZb+{BKV7iXu^nm7T&uGMVo-U3GZ*QBG@lkNgnVSb&jlRZQzDS{V$Gevtn4elNWwyW`X=bCe&lnrTuOY5Z>twJ^ys`OLDll2eMJ z{7sTWihY^2lGTc@iHjv$l{ry&CC8K_!3fD-m4i=!WS(lK(@ANlD$nYYbhq;Ke}tN6 zVwSe7tXkZqou7{r_h`>$28g-Z1Bn`OpY~wX74a7BonVT1yDq?|P&`X}#ObF5ssUJ= zOOsT{{}Gs?Y~%aV&mx}je14p0z&I=8lSp8!PNawy8eO8!h^}^h4d#j-bnW+nh)x;m zox{Y3b>`O5lEW&=e}u}5V=J2~i|&m61$@h1F~V#YrMe8ebncIvHuwnoHQavSH8R-W zWuO(~;8xlH0(Z>zQBNT0iP;AOk(pv5%olZ5=8Yr*V~W~_PJ!^*NrNJYB4zD>sjDpZ zVSgnmHSAR%1~cLB-+Lavz)jKHObWBh?0P`6H*eIP<@lKhQ^bcWU#|k^vGlg*1Kd@UtPIlX5{7PlpDfCa7q2^1q5Z=1UyO5fd zALv6O9~CwhnaU`zUV%)v%{o1 z1ZSatjrJ7J$sMBg_xEKdGfeIEN&R)}scH#s>_`;nIx zb6bn%7luhS{|G?-L`|ho>pov?5Z$%ktFn-KTf9-)%eMTFpwVUXFXe-@%LG3%?`s`} zACf+32%;4+$4qJf_%OMeC&~3Usvb(GyWdqs$XXq26rbfH%M?YaVlrcHx=K?uccjzu zoi(Ylkjw?@KH1}>6RP)eP|RkPTz)%DtD34{`EOA6D&swZ6nB(69O@Mo%3YQW`Ir*$ zAED%kTA?!L?NL2e=b74xD%A&)mMT}NCt?;W%QaTyJ zN9aAD&-{QEIQ|@g;C@-R_dOt+_QPF+tSctMD{)7D_WXjj?O8!{n>tP;Z=S6NAC0{@ z_6)WxeC13cQa%+h8h~l`N*DRGA49Lmvyd?d6nxoVCo_CDH zHVuaRoWEuZwD*OENrLQX?xvA@xU!6s!$txkY4K1xsWUcg;1(q!ysdu$_48C@pM-wK zYfCSIxyX5{@d3x%W|h8DU~TqQvsKKQB*@Kjqlb==c^RTXCAB6=K9EEE8SCEng24-i z^d4mXo~rLz%kJ~~+im2=JBzx!_+(ob?L*N)bAWo4^xreD+$`taz!k>qjE4SAET1G~ zpN!3lwd~E{EDJ~XOyxeBy0Y7cpX6Q8^+qt}d`@o=t+qvLHb{NUm#L1)ChI^>3t^u)uf`H+x)*VAG^l{Z^QA%9^tF0$Ge6^zrCdftR%%HP!}q_ZM#D~Po87` zMmeGI_#a_+@0e(7hFuR+d^RbtyI67{=C09NDhelbS;>m0J~DXABfR(PUMdn?=$Z?P zM|O^?CCWmJL}jf)_&i`t5Ej#p=X~uKP-C%o)RS<@a!+exqu5>V)pJy3zZK zcB%S`%TD!XHOH=5c}exv0;kxmSolA}4C4;X=k#@5C$-?jhlYLHlQDbsdRo*rEru0kiv9l)26KSxYU!F89S0g0@*_Gw0*(?t zLc#4Tjw6Hv^XuVS`U1{PuuTqDcnJ5y3~=hrWZ&4y{L3 zE`YTiVcbd}9?T>lK=rWI;2MY@@@#_(>>%c0Q691$zc=L~nnnVKcVi%wVUHXVi+aiO zmO#ex=;^Psf^JkKHO3;E+1py$P%rS#ZAL7!Gpb`V{z3f|@GeqGAswcpa8eQxr)Zq; zAmn96j7I?B19OU%x4@s9Xxv@dg=?2Bto0|fv2YDbNoCldEs>NUP+r?Y>ixQ=j$ib_ zf|KAs%+TcJux6GaY#)4-z03U%em)0mRmDHdpXdrG-9a;{SywJG^$N$Bo_`21?;Fcl zt2z(>EB5i)g}{ZJ>jlN2^W5{vwU8{{`mj0BYkZxwP%Dd>=z7jh@yvqpQ_j~Y$OAKAlvEs!NUoAd)@QYs2N z1SZIKc|62ylfAG$#4VGDX`^!qdVAr)q9C1=mR7b^`wT^`4$>9^A@z98?3$QnmHJCQ zux*MOobj3l z^`SZ!FB#?_=@sa~O>JvxJ`I>vUu~@F1Lb!B+3ei_@ntWlfwG*fO- zqMoBhW^P&#CUD7}InSQ5q)0UTHqyHsI@a4puCW{KsNU7UA5P1MwzLdpBs1Dt`WfMu zI@rCly&6$DT~J#`wx9ZlVH8qPYmfk;FB+HAf5BG*cA`1RG1F7)OXv}Bca0NP3$M+a zi8r8*C(?-x*e@ZSqc6xLM;*Rdcu1vuGbl$fg5~CfG5+FXpV4TkTb>^A!g=73^=>pP)#t zQ@lwKi)R;HjqK*#cI_sC_(sbi!B^3KxxC^Xx0BOdGn*Gfgg2z~UPJFU@8&OPnc9{s zD6dHCm?wCeJqJ8jD2_)$ABt>)$%q`0+VukwC7xn6Rp27Em7&Wbq@nEoDrac}K~ei$ z%7ywiZj$yl3jiqTit@d{SJFM%4V`wR2|+2?}bGukGL(u zUsH0dzVhao+$QH2F7JUbKbB5(Yw$i*ImU(H_jR3J%bMmjbr?36r?Q8|g9=l>bL&#W)u49RiEimQWOU~R2y|IDV=#<;$y za!-JN&*AJ4U~e}+egu?h1cd+~tGlS~Ex3g`gmpi6mgmIW-=hUhQyWr;Tgtw*uACBMJq3^2<>4k*%u-!*-iH7OcC%8GP=W+w` zTkUrCSM-d=-GnyGJitcCLR?1sVAD?gEikZbIgtr>%sN4iLy6+TDBDf@0U>n`!PGLv zHj*mLzDi+?4(WQx!PaP22W&w*755II>s$*)qvX(gjjJ&-#P8CRI2y(+YYG92dmFo$ zB=by^(gNp^47&ek8%$#b8 zDHC3E=2AUG_f7eWSCYTH_?o%mIP${=KQS3eY}Sf50N1qM7JsbW+2JEeD_RMTlW^0= zp-&`xV=(Y{QroHV=wKJ55NK7xZPwPK*J+f7E&xKEVq^>vR79Es3 z+26~S584rLRw?^0!57yx_swi8Yx3@GtJw_L-xF2j&|cVWm%g?WXtavi0NK=)F!cjs ztpV(E*reV7w}SEKD!;HhOQEx(h@i@xnf363THg_6>#v5-L&s}mExQM&6}|&@^v_CP z3o`bl#GD5I?sW{DhM3kJ?ed%WNpEj8#4lC(@>UV-Y9Em25?vb4Am)%l0IS-TlWW>} zHB8DNIJVG*dJA?o^$a}&6+LYugN9A^JnlH!cgEzdW9ZNb)hJ5lxext8$@g0PI?hJ%E66WOzkr(lNmWS zY&RGQype!MkYFXyLnu7rI=~l$KohHSa1Gek`7;Qy1Vjpn_=U7J@&*}8x$0X?EvF4R zRG3^24w-F{Ugq^Pdpl|98hkKhAGQUu9Bzm2Xz@eV6Cssww1!-tXR5QJrY1ka&!bI5 zs)!Mc`Mz;vJ@c~zmo>tEZSEya5S(R%0zZ-c* z#<7(llyo|`lZ4kBpLVv03d(0e14XNIGvEirQ<8&G4&uH@2zsN$#rF^%A{lVh&=*P< zTVO<=WLv1Vjp2$MbXiN2!VW}iW619}VmhSq8k$hhy3DqHg zyz~fsKre}-B`y%z*e1Qqd9O~OD%g3`lxx6a~kqY zBTD)Sr)wf24ajo!2fq?*zxuUPIITgYx3m}DS2~jq*ADlEqk{iX2>1& zD~B68e48x>H#D$X>jt9BdfS)wUCJ5ie9$|V^aZ-2M;EydzO#Fq-$o3kYroSNm8|`3 z$rDB>+o>C99(9T69NI+Vi;f5MD}bd9s~7|A@Um%4G1xkr!eYT@CY)r;kq^SMIU>w8 zFFN-ZKFwAtawPqG?q+=@UnWWctB{LGCx8drnG_4q{rU=OEY!Z_6%B(3o`I*&Lr+U^ zV%)+`gx+MnAoO~1*%wHQY%d5{)W?(Sf?G_Az{Y^5A*SF?Ll*(v2}nd#ofq*vYG3ga zG8bExHAs1bKNQcRb&vu>b#xh}%uB?4M6+o%xx>q7dUPC=Rsp&>Ct9{kxakw0my1YvZ2hOgEb*J0MKJYoYgv zdgwER6WP2q6P04JcEw}rX>LUlTmyYDQ%p!@I!@n3e92;kW|0%vhrBk?=5Vgtt>vue z%gjb(mg0Ea;?AcG7StWu!Gr>M@I9>d>N6-CHonLblg43XM&k;(lIg<)N1i6MhS#&u_U5RSG<+%iuo*iwkF?8ikn|_c2Q0 z#p!GuQp67h;kSunylcp>#pCuQ)^#b?oG6(iTZ7pSjF)}|>pT9D-fJla>!i1;ZomSi zZwf0Bb7gKBt59EMdD9V?k1}m29fy!T_wFM-m6Pmuu|ni$%=b%N6b|UWtt8F7&e!ch znz-hZpfl=2RVoNWjVN@1d#n92w8(j?$J4>+y{ZeLz1SAjL+>jjZ`Dr+Gp0fL+QM2q zQvpHKTF!PCc2e7hjId^8M{}3Dst{~x*jVrYddgsxK}Lx5%;_N1UcF7|DvVHf$;X*U z(SCKvVY;d}nfA2l3N`9)bIahdPKVZ_f%ay%cK^O#RSux*y_*a6L3Z{m%K*ZAy7y1- zK)&j>2;Gl9XC(RL5S|)39f*u|+CU46si$};<}`;~hX(s|UN%i_=5t|yid%X+iyrsp2+c&jl{!6%!zCF9dXw&ia|Si?(T!z`Kk* zMn2Fv+JUFMgC;aO&;sGd%2(2>P|!RNh6M96706s~(q%o*x=uV7NM&Co|8lqBX{h5i zDWWju*-3(66+%yNM+(|eL=k#HLjvg)_F&mP3KD-i7e~EJe35dEommJ3FpBiK~xEE#T+K&1F1M1@l!p4;7(pxmPR~EEy}q^#?o%2FezUc z*yutUg83w{oX%xexi_;*I7X8P#4*9YD@%L@hCx21ooHK#KE|-D+mBts%qm@jSF%t! zJYp@|n36#H!`U1iOObGI1!AcScwgPuFs}(5Y!i6LMIX#UWM`y6s0_~G*0nxGGI?fo zv1m4LOKCb*#K-0Y;Yotv!M$i7(naCAKn2A~q;>zwm@8)5s=2SFBr}ODUUmVp z4caL_)>;h5i~rWHLP{mMCD+i4C6er$*lm)t$*1t^q|wpN#184gz=fo}vNVr8`l#%k z?L}_7e2&>!=?Zxl#1TB83I%M2E>Stu&VV0MzAiB$KPV4pufR-GZb)8_t59x?b|ZKz zuLd3^u2MyKsAvaN8FpTrxys(jyuW-0xEu7yAOaLZy!3x-?!YSaf|5prO!qWffx4{Y zCC|b7>LQ}O@wVEZflmmhb&ehfXzrS?c6FQ(b%J@hWL$m+oY(>G+uicKb5HNingOUw z&#saXczyT&Y!q_0@q2PNCe+v-?Td@+dJ*`S;M;}pct_RhW9>vHw*iqkQ_?5@3BDsv zsgtxk6IV5bSN{+L0jG*R#h`Xd))n!R&hLrs5;qt%;+pjJmnM2=XgA4YVO z;OM+tGD@0dl_G1P|NC@NkOfL%?E`&jn#(qo|5twHq`@-^Q#oId*D|xX=P`tYDZCB1 zj}a049wN-IMgSv^I6oGxrQWrYNuIHmOva?}afb(eIzrR9i{XblP?^hg$9yUA0W z8Ee^5#N7DJoK$jC_%m)Qwazb(=SiRH;vo3LbhkPpUd{bChav_8f0N(f%^LopdJryD z4AF{7Bl$n+N=ki39%CQ%cRY^ymcBgvINOd{;Md6h&D!Ns$ER^9)_$TcLD%FMBu9Ww z#29KyLkLMq8!P`!9;biG*HMd^dFd}`i&&cYH2Qn?`S63x0F!Qmj1|WNxUA&a@$XuT zg+_70Bthod)`C0ACf6$oX`H5V5pfeYIX|3Sz;jGrL^;5F6K_w8<6jToO~(t)`>kNi z6JB!p%DF1evc4$TB&AOhOu5DNn03OLbzg7^BByc({C<%v?=lf1dY4WhTZw7YUr`3c zzr*)ZpGsExU8I{brLOtxOEQU#KmWV@{}MbjVrQ8P0hbptpQe39)5+))jaTzpw?bynj?X&2X|7c^RM; zzNhx6eP;bKCpi+}pem{()|^wmZZau}+GnI}Q3iDGkGZPMhD{H#Q_e(! zyyI2Pm}rMhDj0sQ1xVFIl}*khe|z&~>C={D)u*MO+HMzbm44~?m93UOgT$v;$pYcU zG0ieAsx=kyZcY=H-ex<`lMF#1f#a6{G#=r4k@(^DovI3=6JrU)A9~Au(iDBI{ zu8S@p(~~(S_nE8Fuf&&egy1d-lJLkoP3k}nb&QkM(%dbr`EA`~Bmahp-c|BkCowi7HMNTZhs8%QgH@xlqp4(|fde%h!bL^6+=X<02Z`K3$} zBy|nj*tOWLm3a0m{G7t;oY_P{);JeMu1VU$OQCv4)A{DK+rcpYQAVFPUa*Q4?zmFy z&QVz|kbDySog_$y>Mt;6k;f|bFk#fNLLe)RwlGV=4rMqdu{mx`UUU=ZC+kr#k$Z;I z;JuP}jl15-M!13h#L7(kNlcg|$PU$)(NdWDiX6HxYgWMvh6{UHRy;G1vnnZ{70cZg zoym^poeE}gT=;Lj?{mwA0ZwfKnK;s_MeHXPOcG4l$>S6sKBoLQ)lJ}8pr-{0_GZ4K z7YboX-VB1sJ$f2bFS-(}V#&mte7xE3BpGCY@K1 zL}`~=NpIX*w#h}wO-g3nYt2@HOihnC=O!>_# zge9uKWnYOq)B*X=NY~V{nMR74IwJ8A6{OCN4x;T)(}LI2->5J8h?(P>0Ov677j>bv zhp<@nzdTOu0KU+0rED{SXu#*MC5{-ZGYO=l`VEPfD9L(7bO3dwUKG5a_Fcc+XED=H z_ujdgi`Rx&=Lv4BjQ`z5_0KL+9lE67u%yW~*G2yfuqh3tpV3}6y;b+D^I(L%?gA{r z|F&)dS>tv=UyDhyo2uPKAe#NvsA&IYNCoe!erU5=$i@3LhufCtz%{^*@zgWw9}wHP z`|5w-0pa`ApHNf%k7%Z2zPf$X5bz7^aB3ZSt~p5c{J;H!NLBemDd|ua{ZYn(5!q)I zUtn>m<%*YxTXB4)Jz5?vQI_BW{WmJR35(s`l^4m@cB_=5w1eiWlw!{6$-59gulOtT zgc*xUq%V=q*#**PXl}{{SparVT(As>{}o;(J50*(Uo7{hfZP-1Y8uY&k^BMkhWUF% zE`Kr;WF`%*$Pm53a0-`;zT@W2I4+JPtW05wwIoCA1MxQsAv|6}qgDEEkX&K-x#Oh_ z=0EmnvSzNgMVZ`B`0vV+)s`Csu_VvJ5CM_=YzA6zm^wQpL6}Dmj@>Oh!&ng>DT1)l z{4a}6v5&a#6wl*!*pEuy32+w6WK+Zk|NAV;-MEM8TMKsZqL=|QTzLyw_~g(0GKkPHKdjpmFrp1^za4p2?v94Trps&65mmr z{Etw&o6)3z<}YI$R6NV7U{)#t$r9#A%Q`1coutHg$+P+#?+1;=Xrc}io-c! zsA`kt9x+g<{T~5AyQ6)XkECO?U$gw^f3$y-(TvqP+gLbrl`bN@n6+Dn3sAB9b?ZDW zxNEe>9IXUB8jR(0(HrH`{|H6ng9&whIi>wi8tiim+H%v(yW=~w@wP@j zcuV9uqXXwcT_-SWobwE`@ONzzbrg!iY`W$=+j){8W);SF@!D5qTNwsA zx2E>%??C5HU#BM`0Ff+x6xuq-T5pft;C)JONho$^YA47^Hhc}6K4vCT!?}}PsAiHq z1tmIPC}&2q7K)gb>ZlnPo@MH&N(u2ichA(oE_;>B)`C|wq?tY2;P`;<)0*OlLWb0-UaDV>ba~K>3q5(B|+N5bc)+4 z9b-WwK1i>#cbXJDLb;|!0a+Mtj!VC+NHAo3Mm8iKG(Rc#k{l6a1*`| zKr-hDcL`@EpBJtYEs0YLkBVs#??lrjB|!zEWzy+BFGVM0#jZKxg>s>tP_jr-Z2^?7 zSB(5e$lW0*l&;H!2s~vAl2;0B<)k=U~qbjuB zaq&N@Y>NSDq^WN3-+9O-@b;b1woPdj z)M*W_Cxr(!yX~#SW$F_a2PHcdcm5+}Wz7kz)uf;a* z+Al{Q9{dWn4YnE#flc$Z9WWqM-L(BXF*WwLdx!*pRZN#V)%2f2YstAVNs!cM%pU3m z?o7rFdVmhbo$WslSsyvK?l@x}Pq^&%*qBKMI6(CZ z+6Alq+9tM%pso^Jo*aWrmCot@fE-OC8Q)=?<7|!YxX#G1E|f{0?yTVrsmC|pFiKhH z9FTvmY7+p){1)mo5}RVE&%RmQmz3@cpLjxKa=!6GEnz{vOnm7 zZkRUc%hk0osO}B|c`h#iSka@0nL_mUdLr&$yTP zQT>iZi9M=rVc(1NQtju)1ly{*d0gKcDlY$u#}wsxA<6NqVvG2hO{T(6`o-jYVLs`1 zAU9oUgDMvHV&X;RLw;54CM7^HJJL;YT(~_rNU>CO!OvAOPyEM&sqmE&ol@l_xuwmR zJVs$}UM6ps|1YA_Rw<04=ZR+&UXsArMe>=_tVk!hn=C0fSvDaL_A8WIDWW|N%a$pl zoqA+Iuw_-H{H`RVrd|k#vzd(+@A* zp>g-Ll!`UcPS>SE^>N!I=^=H7`DPhN5&0j%%q+x86jvHds5zSA2V3WDkZbTFwiepS_@UA#G9D;>AmLFTK3%Gx-? z&-t96dLis$)TOw~30IP@rd~_Co_-_!X8NtP@zmSNcM|W$-;2E;^-tu3(1$^f0v=C! z;`QXu(+AIvJYW7o|C02o{&n&juQ%V`-g>w9{elmQkLXY3pQFFne|h!w+_#P2hkpou zB7Rj*Wc-f&HGIGq0ns-^WScY1DvASTr&ZftfVpnS)=iun@%JGcT8s}~os%xWL zio1{d5087EN4!>f_xtdsfPGW_{ts7Y6;{O?t#SW$cLymcf*>h~gtVk0B1*R)UAt%Y z?AbH3yBjvJ>F!iYMa4ix?C$Qa!#S7d=DVKfnKf(8TI+k?-@+nBd0Sj$3t zk@g#%^SX_CJpJs=`wVOi*BkybdSZOuq|>x?3(E|@^@#Z)i@BepdN_X+QxV4cuL6e7-)aHY+%%knSEbt>oL_!3OrT6(K| zrpBV;U?r`pwfbJ|qB_U=lm=$xgNUwHHPF#C;WA06zTQ;|S z?nK;8yNA0EKj1y&JSut|{lxR>mS;-T!p|@BMfDmp0vC`lA^YGt+0;&z?1V ze2#o>+&r_X95A>5TeyFb{-TeIhnEPKCM|PZzI6H16}>B&t0Gr#TQg(L)wR{@@aqFM z=xzA2@ysTbR+hGh&U&4pRzU8?ucuee|q@QgpBM=Ad8b-lGBtsly@cnX~EB;B{&1T zBO#cWOrnrwly=}E_!^o|Gl6~SM-Ud;j7>0pu(a8`IVoHYuY-SEI74JA4w2xbWwNoU zIM7mg7w41|tIw2vDc9C`RHRi3s`{!Q)h?*Bu0PxWH#RrjY?(C`2NCV`j`q$6-Aj6Q z_NMj8`%fPGFrYQ)H&i%WIdbLXv{PG0!%stJ8qQ9R{XVaIVfV#^ap+R%<^C&^6K^JG zTw8m6%MGWSdu|=PopdMrE@7%kWK6|@_>t_f;)(L9@|ohf?1lIx?-lbk{D$;4|6Tg~ z*biYJ{XXsdZ1F|s>+)|izkU1u`p4s+_kZ2{{qWE8zaRha{dfJBIep9Y=QFBjrp?+u z>&NVYInZ41d5h*qTvgaZ6m6&RaUZOtL&`h0%(4E4x-vR(r2mw&v#A z@^wenTd)7U;p9fvrhQr)wH|5L>SXCU=uOi*qtD-b$iT?pv*EB2&p6J+)^xt<{Vm;Q z+^s3*yDhXVer&sH*=)two@niAv(e_O?YLd7J>(F%!`^YZ<143=J4>7kcZIl^xz2XI z@7Cuoo~jG}o+e(iydHWF_^5mdei3^d{Wtpm4Y(h8BB(l;9g?^A&_0jQZTmNb%?8==_+p*qFEj@c{`QiB3t|lTA`|Qr8?=oHjGF@89_@I{cyi~N z<#YJOomaN6d2imn3wYo3arvjb&o{q1e=GYj^Jo08lYfl=a{o;Q!2kXYz4%1=kFt2; zDP;y_)4=tqb4%^m)VV`+eCqV2v`}VI4jd_`%%&vl7gJ_Za$QmKHwwj!Kz>9i`Q1ah zhW0y|q)%{F^+WOk+IQhXvNkP(Y(ut%PNYVW{UP(vDfdAt*X05^ z6MSH%LoNU}{}dAM2`Eo!#0R`R<9y;P&d$EEDc8V+s!b$)#&bT7LYKweE1ediIbsoWl+gy(X-3l)Ul5{sU1#I-`WB8TY7x8hAF z5;#8z0^%{&?PN33492Owmr0&z=`KErM$g0wM@#(}Ze0kDNB8AhmSDSd5*||%Xq=oivNh8I;VPBTu70vhW zufuoMFCGiRU#Uv#ynugOzOIZ(3+(~;Z7pMJKYUvKGG;!$ zy)vrs7-6A$JVAHrdAx%)gwvvC=WwC{*K&)1n2v3F{~P!0Lh@}b+>=rJ(Olf~L6bIJ z+}EDa5(m6_>q!P3pH**L@E1Q=F%T~$tSa#c4kDyWJDj%?Zt|_SD2dw{>)t)aUAgXd zYaVWNN@|sa8$OM;YU9ofJ}eHv{qD)fdhw2}(+eW-k}fxlC{cIO*HqDZ!-mT;R{_JKn;CZ4==9^Ov8dgd=JhBUo%9W+WhTxJg0lUmqb z)JF1LoDbEWd^phqJV$N`@d2kipPcsruPED1Wk42%`YwdlO&8s&gdV}=qn{xw+Szs+ zY8P$28b@`96wH(0IAm4$4a}yFC+LA2shPo%z)9+yogF|j2$>!L{sX7I3570mf8FSW zW^nXQt)%W{CAOx6R~c7}Z-HqTlMx0kKqCu!fC0oMehomR8wN1}2l&68hLrcvNmFf# zlA7}BFk~S&yLOHmFCm?90-J;%o14H*e5J|*=;C7N2Ox&c%|`$|W@DTV58E&{sZ7NnYTS@1M3hoa)` zh;^gnvc-Ys6d%SzCr`>o)WGBc`5wILxdC{y{?`Nyx>i>XO#>=5g$-rE0yQcJD7DIa zbdVAy^T;Jowu#QiP$-M}5dqcYXY3nJGswM+P?Im@e0s*yGEmVOe`N#ktY!B=J>XdH zT^~zPS3$BUid*??7$W~H7Ufit$K_tpH_7ed=>gVc9Dr zgqp4mGqzjoNH;)@sE^=nh;i0o zS_IuL^86I_W6z#Q>IK??9h;~#p%CL-(1_}E*MLzd$s4y5z zD!D@i1pkrj6U4%Eh5bN0O^d%XV>cAWnLNCis%GW-Z2_M#W;rB)+Yrn+0*Itd+_J|) zYm!c%K;Bp6btcd?rE1M)n6CQ8-%R7n%P9U(oh0(;A?iz^&7n-H18hl7)CUCYt4{T)T8mlNa=)P;K(SZ}2PCQ1ek8f=qf=_ols5@x< zR!`16NLVi-im6qVuhO)^XQcrLkAaSgS|2Vz7k#uZr(EXlGHxKh!<;4;p{?gFNA4gS zPKjFvVa>qnl0jO0j}_Y#ifnBsOi)wnZH@q7MMdjDZ{TZ*wvPwkCM~olQ3Sl(#=YbY z#(!5$(T>Tqkr~LMi?f^G!}L*B@lTrb;0#tiWYt|uu%p_y_#XKW%&eIRe+dkf`TEQQ z=E=M4V<^#rUB)NLFhet$Dte4JI`L0{$3txeJU{%c(t#;br4d(`)lH}-shBf~yVG7_ z1;lF+TaiDcWFMGLBmJ>^0IwxuhEE_d#cLu@sHDCdS;|kQX1A!h(cllY7F!9@*^SI8 z|7Btr;~=>0$QAS^xcs0ek_vqFz6SpQZrc%PV&J&peP}Jvb-7+Jz?2N7@f3{U=5?H6 zY*}$R>mG88wV&xte@QUG`svG4FQGoLTX-)0AuZqA5yn9^cKS3O>K((I)N1O|ODhC( zL~{owxv%(t8aeDQ+`lS&mJ??=b19>Nm4RP~nKGWFJV1uAxC8CIos{*8FE#RvX_ zKQbe|tZ6b#-&O?q(BBz80WZ@87wmc8Ds}rmbKaN9>yEK!mTZ>Gn7b9DXdBioZ7=+Z zI*B@yjp%pyC&D6Oj5FbRnr6woW1C35iGDPi35LQ~&pqS~HLCmCIGWnTS{|#q(oP02 z9+hoEI77z+o%MjnX;EPq>f7e*wlhY_{)qED3=%}XEvy^a0T7X@=V;V zYDs&HT+A2ZTZK!RKi4}9s3Y5J+zwXLz?YjgE^4utw#_ya*TlmY)SDS z)luevCpxdlMBx5Pg7_iuMZ8>?160sP_~rnTC*pht$O%rYD?t9fdyD}fb9W`$1Y}#| z==}g_;6kebT2G1;W!TS-DCt%7QpHBG1=1!Q6r85lKwKUdzMYG35@6%_aF#!w*$Vy*lE|6ki@sW#7`{Im$<91vgvT~-Md&;7(S%@MON zV;ADs%uzIZFN&Q&*0{GL!|;&xH24~gU=RVFfGmdF>S2pUS>!n`kdYF_V)i1(PyrrKV|&f-b{Ok zh8d`+Bd~bTR90OV)NCexQkAH-5W1Dqxi-9sl3og)Q>EOV`JKg(_Q%9A5RqT-1C+_T zoy%;5apiUOBK-2DK$MEfRM%j+d+!&aZXlY~}GQhRAGAbSac+!rG8^Sr_}a3Py@YqP z?kS1Mv8eo%;lr9)HaB`Y_CvKk=mzpbYUuh2ej~8l?hid>?=$!as?g;Ahcej+zW%(p z<5;)yhTwJAQ`U0czSdd9YR>%n(4$o5L}g`EG~p4Rs46?_e?wG4LP4S3Pk} zhRKrs+m}H(+_#1+L4B;Q-?zdTx2FDCX#}oOm8qJCBx8fKm*5fVhVG?h zL%f3OJ0)8=_hpreD{OBpK{~-~#_tm^VT7mU2o7Ky4(D^5k-GtO_9A-4u5m^NoMY*N ze4_2se+maccAZztjAh1E)y3uFmr}AKL|BK0NkjNYaIqpTH|t0#|2CU_=nU6^SsO5u zCC5(h!ehUYSC)DRj&87dKJ5%Gtb<#6Qe9SwE8eZTCP|ckR7^vHC2yn~iV8)>B7Ev3 zKb!yl&}Pm>Zh}9;+Qz!I%K~FD_FFEY|3hAEcB1WsGuz`zv1<2(FbQ%iCVk9hi$Y zqfjKWyVasJy(^*ILp9VoDvXxyj5L2mj!8Z+x7xTDw*E+DqmN)sfL&IiRY_v z3O~1m%2^0T9RDlc;zh7UbdiY zrNEYwTAaw!%g&Ym;)KL^Ns3w0{jY_7j5|I~+#i^M<2b7x$u}2bFnm|9n{G|>tzA&l zQw%A_Dm02~ELCZ{%!mvZPZJkp)yVn6`SJ55mb}*e(SkdigWi)|25W|+h-Htpn{%;Q z2wCqltcLg0Os@ed%H{Ve4wda^o+*tknL`>>)hIeLf68s8)p2>^%c926=K>(RGW{j7=^}fk3W%K) z6J$-H$pS<15^p`u6@G@J1+$QS-5f%#8BzKnv`+fB%C9wHZ6~E~DpoY5V;|K&Ykw2x zs7xx4WMs-TWvgTL#pbFD`)=_kWPsOk4q0Tm;|o)t`_w!ixy@|X&!-(i$}3mbc=rC5 zF4Fw#T#T(&-))&juvh-68#~%1ORGE+LleC&9p87D&sL6mC2;H|XLk%UZt>5Wryv5> zeSINqIl8?fx@Oz(AxW6#b$=zApdRhKh36@+H~%`SD|4=MkC`Rvtl;nc&JQiQ?xn^4 zE8nz3&QJ?8%p;Kl?7RAP&^5HW{8al798L7M*%&v91k@4m8*%+ri}9@V*s^N`?P!nU zTEfY_Yvm{r=Q&NBP1@z4&CejOG7DyRQ2x`ogFOQD%bMB^fmGq&rZiwS{YdRO$}nzv zWdx-@?PQq=r7dc(cm`!Gq+0fz^2MWS%Dvmd{weP=kZ0z}UJi`u&ckd#@6uCk2}q&P zsX}Dw=6yX(pv8ZD-UDRT?xJvO^XIO-Z3dUeE&jHl7Fdn3)rcLdEND zLlx9e^+T;@a`8}({MRoQg5hp*}jqMl<~~2 z;C<3?#^&9v!T?lge~{~gEH<-c7Q$*>1R>IPl`L-QQFie-4eRCeA^U2!B%@%J#!&b^ z)m%M`-*y;NyyG&19!egw<9Ejjt}~tOtvDAjJu_FvTSQy;0{tOuR&3W&R36G5s~=OZ zrb5-$%BT71-7C&? z)(5iy#!_rhZwLIA-mJ`Rv1$f58|zc*1Atdm-POx--C{#|k+;=TNmVl1#! zJS4f`4)Hq$7wv}F6`X5kd$3lj~kqZi)=D)}FSOfLu-BZ44 zRq1VOaw^NtHwcpSiz(Gk2Y<*t%H9V&6kDiD-DmI(r2FhB>{WtQW?q;+>#bfHJPWmx zt~)jt@6536O2*SjX|2=o6ImnmzX+BI;ngOD?r^K}C}NoZ{9+Ahj_VxRJJKl|e~~+x zXEMYcr6g(BGIs!pl0*ILfDx>qQx2pPdt1DKEm@ZJ&cN*WyHy#$>;q@ZYJm-V+Et$c zZ!)!mcNh z<&4wTBm5}ztjTZI3Hm*qA#@?USn#>WqZFm@Xd5era4n5Q1#~pEhA8F7W@tJ@_x2B! zd=|L+X)CyD)tKNcHT1vV!D#G8(phA69M5dfq&1uswP<4259tfs~Zi~()1fO zn^bFJ-fIG7llu>r(8V>rm*uwvq+PJsl6$~f!^5$hObwZrF(+LoWDz}+FYCVAaEbP; zZFkM(qLhZa6{pi}t5248#>AGtRXqxQTB0M@_a(`*#c{j#iO%pf)&ky2_B&H+W;!EC z_Xxd~{+surTh;cB=H9lrDW~vKLwW6;w2Ep%rCs#na%JiHP;&98D&Kdze64KLE+bKv z@PswQJ;p6G-NjtTbkpV2r+5ZD|L)}8#ZYhSz4m8?p$$1rZE2R(F15MQr1J2JtWdLJ zvYO)aO?FYi-ZfKXA}+Nq@vKHvJ(AOj5%)2MI5j-eoj^85`=1z8>C$y$4 zZ!;i#J1l6ZCuW93Rojzhd)b#=B{evCsq)CNmd(;-lubsl{1M7!Eio$#&@j_aYzB`~ zypR0?7w7Kl{txI+u53L3fQRks9{~j+ORI7K*z;G}DxlR#OW6awvn-W_f?JI8`7^)> zt;@^?u$*ye#0Qa+$B*gITXMd1S-=y?ds-CmfE-u!=oI&g#!$dFRMGX^i_xzni~ffcK~}ao7;e48w+!<@-s*K5xnI z?5d6kktRv2X;csr@u)U}w=?Lc<}JtB)3ij+@^@6rJs5G8sPG<2HhRJ#(<`+vVQXoh zFs-2(>cymmz1LM{*>>#@<^G9XO-u8$r2u5 zEHj?Z{)7zZY(NoMjW!O}R}2xQz4>KfStr`_ia#X!HC|RoBDU2WlH!9h%b$sIJ z8>%)>NNt>1e*9ogHCQ4Fd{91HLG{Q{wM%jwgQTf~WXm$aLQcAIEUSgV(CJ1(5iMlS zz_Cte!j>LJOJpXwjZ}X$VO2v{Rn|eXYRj_Yf%Rpr#l{}Nsu&sH(OtSuG}E%0Kfq-h z=dn_l4mvlHNu} zDR#6{Tgp;%ZTU}1yTKaaalmbpfGq8dkn!fq0y~x zfWLk=^&Q|W*KL&?(A+Lhod7188_G>Vsez;59ay{R8tV*r0dN=tQEFcE$+?JQYT$-}I&?~!?k{YPi+)S1QMHxg0?5N+hma&#oHGs+4 zb5ppfk`o@R^wgwd*BSSsG+io;8@jb67bW=3uX95{*CiDO^lH2Kk{R$Vb4%%Yc)mda zUr006@?d5|36y(h7YNmP(IW_-kZO5Mi;IuS?tH>-37yq^jHT!MwiaX5yZqB+Vu5zC z#e0!I=I&B0dYwTf?T!05}b0UKSy{w%AnJVuiQ7?_plFi&Lo?)6JtK>8P#2Bc3%qQ|kj`740U)>h$>%o!s8x0Oh}{ep8a*o^MHw1+TzmvV1*fvt7Ju7W1}wq4+AMFkHh`A}QLg7#S`gzd0SG z@yg8^8dJBW`1kEs?T%D;%#<(PcewGgWSuXuS}JsKIb8O_|wysWdAxygg6Vzt6C7m)zg?aP=(l50}Bx?}BDK zoZ{^ihq+TU#&R~y<&|!SoSA!7l05J{pO>~>` zU0V>cgR~Af<-CU!4`iCkNp--=_cle}*@3rRaoNn{r^9hC8Sd>MJ{Ena?!cc%ZZh8y ztm$`(mJ`(Q!vqWBV%p>27-AuGcV{>8HubFOA(A7=f9qH@OLFJtz9N<==@bz+Pq3nO z7?;ESQlf=>!CA^c@rkUR1^4hz8HMrIge0^*sD$v8{$!^$F$P{@YDm0B{qZ`j(4$mw zol$t9*!9Fnk*{KDi(XN?R997jvk|3Y&bR^oVE!q*Ki4!a7=N234g5|>!G1VhAbda$ znXVyb(~@7_FA&%5oBUd6T(x0jdtp=glBU`sr!mP-tY$LLw zXk0av`w5pWBgWjrFA_Nge8N|A-#K{`9GS;W?Fs$Js%Q7}&mNagY%Xx=9v@^D478av zY%TO{=#?)kyju;@Hxy-;@5wd7EiE>U@x-;r<^-_t0fOsJEAg+`qoy8&T-5F}uD|wkmS2-xBq#=(I-H{}?Vxx!AE5ZzNo68jZip7G>Ze--iNrOp+(5$;QLv6X1~tO7dUP@{5beUHo7Dxnw-|Pt95KUXGE(mb{BahT6$i zj3?Q1$PQR|)Lya=a&gZvIgGwzM-n-jCNnN2M^i`dO^`i`)6efG&ruZgj*~7)%d2;i zs>Cxy7fEt~fQlnYc@~-fNVV)cks#?LlfB1`bO*EAv5fSdK5kr0`bsmoYfEabsu}Ac z6_r2eaUprDT`SL%^ptmnG}1a*H5g9XD8^+jCfNw0508=#aJ~HMNO>&XsS7?B`p39~ zG&M}Q-AM9mo;d4CnqBYL^@KQH^`wGLtS|2pBoeDjI)Er*lcGJtnRroh_OJo*yWoM} zPSSeLD+lG&)_8{TX_5tf;Fc%xchB$BQ);aCfzDE5W|K-2L-en$;D-@?E3Q(qh{2`P zkK&2hO1DFNqDlhz-X;$5TO87f_gJ5euM_VhlQ(A&&yN_4HW4$Ad3U4|O}g^ScMxZ_ z^zrr({?+S{QR4JUV)`IaOMU;)e4>ZK!51c`h{z7^L_TN2_$jd%9lQR9SavS%l#00X zX4VX`7NLQMExe8zVdaf4LbL5CQ`qZxlB zCS!{0^N594TTdP*F29h{h7d+Z*Og`wlmnxj1_G~}MA9Z;EgRE{35ptZgae_sY_acb z!X0^@!%X4^{!imyL??#VO%_HiKu&)}Z414+9FX;e|0;IU{fcS@?rmvdk@N+@rT z22dg?GoV2V;piMlmm-V!N##*GeV2g$DCg{hz$D6;(GFmQa$~ZU{vN$}(gl8tjJ37F z3+b0jf6xxXFS(&mJ-nTCiu#wvN&8OCf<8uU1V2LieUiZpYP;PV;2CwAkr@yK?w)u_ zH|HZGEZB}a*t&*xfHOmlgGyNk*}thj8PAC_YB+`-`3YV_Z4L%ap=2I;PXKRVx}9hW zS+~hZhw_ptykY_)isYe(G==nPGYuLN2bau(W(l6M8mMu+Er=svk7;Q*UV=C^M<+ z)L$79AYSE(%K)CsN>d^LoOsEBJ-|9X!K;AM#J+9&nzDnj+vqU4jGlbq94)1z6&DKwwaPaYF#%?AQ*tSwB@YT4qzsDQdTpSD@yNEruzb#%YO@6tBvE`;U;HlsxxzCX=NXY!8xkc~^}TBn38hp2fSKFY5ood0dcI z@5hzy3d4tl6<%pVVRM`WuuHU(464A(;HJ~TY%VvX>l zTFzfeOGC1_8}fYUUTpjLPp}O$Y@dp@70caCg7grkwTikPt~WRZe1sZK+OzUXSGIp) zd@0_bX~7`H7GVmyRk{cAr2i6Wa(&<{g8A{fw3FQ2y?>!W*6rPEsO=1A>uj(JsW*5A ziixcMU&4;3SdhP?% zeXO(A0iXv)Gn@|0qpuuBm`_`pS`!!{4Ts8n&`UK7`L;-=<^&i>->I(77Q(BQ+}J+a z0x2=%Jv3jK=$;Jz<@#D118ZP42CCAQ%`jw?2Db!v`xBC*Xk$>9yKcIy5&d{6^jeHxlU@C@Vn>S#u`){k) zpa;9$xLfGoTR0RP++X)2^EVBx439~Jiqw_CpQ!or2X4b)s%YKzyFer--O!n$gLV$s zFf-4NH`!prC&VSYQH$fQoJ9J$uJhzGFsmgsGmaKo`#t(CHn()-AyM0^m2OVjsQ-?ZNDe$+Ewv}PG`We!Nc&1g1awjgcP$Sf0Tj~IF7jB` zG*%6HL5wHk8aX(aiY*~aUEd;p|YT#57%P-W! zws19Twn~@xfzigQ;m*JkNr7xDG&o~DGYJWdYQY-l-a$UdFWA|29o>iKV6}->1G#PX zq&}y5_qvE$#nE-k1VzF~#RTs#Kaxq{9OcFn-B@L8&CzX)yUcHq?=drmdtfI*K^ZQ+ z@MZcn%WpJG*m`pkH5H+t+jnmMb`N8uzR86(f-SR~$&NV{H>WP4{7>^B9rV z=rPWgKvQHn^R!DW9D^lVUWdAob(=ZhV_J6?L-?(7N>a`rC>xT+ag`-Gm@iwYG{*-s z&&yV)fs6&>*@t=PA^w+u7J3Wkfy*k|U*<*2<4_QKeRDII1&?+52~RZqtn%TLYW_*R zxxtllQ7`tMvbDGrCayT}NDtN_M-RP0XN!jdZ0HgE&$~uxRUA`GHT4UFxcMBogg($= zBh+-XIuuRe7`~rF);#j&<%{eNAxqv%*vwgApLtbMAS0&1C5r33Z z3N(neWU`Q46e;p9I+8lbAHc;%Jm&`DgZ$^SUK3oMqZmA*tHlHwK?>1Jqpu(J=1?a5~Olfj+8v9IK37A?py zB|=XO=TpA4*cacT*_K^b3_|CHvC=!xLE0&C5cNiZx8NQXO#Z~nraB!=5ddbep=)UZa93Ki~Ehsz82&3rA1jQfk)D_YDk z&&%i6Pqm;?+z{rXa6R@cjO_cFaSVC0a{-!5kF$uTyV5@B&w*Y*_=W>2UBytzR{07U z$o(O)mTUu?g=*nQE`#sLM-#=I``pL_6qbl>=1XMkVSIBsf&4?ySOmhu^frBaD1s)d z*H(^|TNkg89a9H$eu;0Z!htcNi`*+`h<8JhnDCrK6i%^9SysGm-__U)_H8E^kum33 zxWb`mkbW%nAuOu>t>o7>s;0|uRYp@^6tA2?c_esWGLij>M^pMG*l`S{V`1^ko1)!5 z^%#%W>*R?zv+XP_XkV}ieHs-{&#n2R%xyJNe3yAQE@piZ2iM#rzY#Q5fZ1z!&ZWWe zc=mOb{{FK}RQk-v0ox@kbNowx&5g5IO)F)(>z7kEBQ`awlp#Gz#X{M}_BX6W;!Vw) z$g2cJbvjx5c{3|r}`f-a)Qa`zW^z zO_zI1KlL>;gGHY@?MR7&fM!l+A9temTU;PJy5d0SBy&-T${Rw5Ws{B!db&{8{4GtF ztj_21fvC=gK98#C+bYW`dKe?**)Th}nCBy?)>Url~q*;0iPx60O)pC!gKa@9MDN@9=lFY!vIw(K^^G`3BA zoWu)VC1@i3^rCR9$tgRwu) z?idTrEk1(^@$VEOL~s<7qV)clP!SdWy7vzshPQZja+tJOhc2cR(wMzR&r$#CGU12R zgz}9QG5ku=h0?yMikn}&i^IatSIlO;Pal;0VN#;2g})g(dvo|R&~DGYoP|iB1BJO3 z{$TbJ^`tfF4#2@sUYUvJldM5_ximyFRj^c(q9ZsXxmaM4_FaPV45R)DVb0c&S3EXr zi^oT{gkj|1%P2;-nmtDb>A|``Y5g>O=_ZX@-7a{f{-yd0SE=v{+oD*xjWjIHPhucy zj!GBm2-bz<^NhJg9>r`+)^&%C3>${4*)zl+Ij3hvbAsd4+M4*Pe*PzQhbDz~TD7%w zW}!s(Kt(?y5uca4MotJuB=drQbH@au9-gc#T+sdnc9rFAcAx$X+orc4x=7zz;-uNx zIKZE#Ce_}DCX_EKUl#Pp6lK>_C&WPUr$}=_hGKPa0XJ7-y88)>Ah5M>!iXF{v&(cA z1JJ`kx%4~55tVVN>nLefd?0c;&*n_@@P{;Atk z8FgC3r9f5H45CZ!z4FyWIN7bloA@%~mI6U{$sbI4%qJAF z_AfLRh?cuF=25S)=(XO|7|KMYJ@_Nnuxvl5Nm3UJz`}@N`8XIEa96Sh^mZc);y_2+ zbWR_*!&J=l2K{ulA%kFn%(h_^1DT?l5)>x?so)@zoTH`H^xnjOsyFoi4vxxg;jn-l zF-)s-v*%x0h)elG>Vo$3}M3+cW z8m?d{>!Z4u*PhU(e89OG-Yna~Ug|Fsk(o}e8r~Hw*>)D&7*U%xVWseQojdR^T8y}^ zKDC&D8C1I~t`nOzTV>m_0@Ze6YQkn^zHmjjgY-DB)89$7oD=98$4g>9wRy(s#+atP zm=Us1cMY5e+lVIXjVqR-2dci7-XOd$zgDcsEG@aMm`cyeHPVm+z0!lCs6E$(jr?d= zE#3@Hq|FE`gqdnOfcBwsT`zbB{6@62?n9jzAysu$-65zgKH=m zRwaHFMsfa%BK60x21%20WY0{&Un$uI;qDV!+hjA#Ip0mY(FKg#x_nwFvP=+AC+JRu zUsnaTv2o4i+Kr7zD@#_@oQ%Dq*rjUme=Gcp?uBNl@8WLxoK+I=cb#3N z+Jx8J=Lyb|W*e{Il#sV*i5QubJg#<^CD9Vv-0Db-D>QG&BFfU7s$UZ)q5%z+v^{j5 zdIPD_r$}*(Y~Z{=5=U;b`Yc#XaW^(*KcL*!x{1{Td`@VmAGM#F+!8~LD4^GuQKubg zsWt{{qZgE)2Fd#>N|L}7A2Y=gFmmS&aSwRd>LEW7j5l^<+k@HKtFdXIm@Vi)u(@DU zvj*Lof3NN(viFE_RW_X%B`IGAkL`0S9)L}~AIdXm%$>Dj9q5bICEhrcZ0yG>qrTGi zMU$x@>wf!3ZY=Pxc_C+dzI|N)OPwmLoM7&ZT2n?~%-DOQcr$w4n=iYD)a@)1G3cPx zFwY5&H;!Y?r5)C0A*Y~JmPdP$SP4Wn@rCVqlG;oBlPNzd1GwXnywZ=H#e36K64qgF zH<=%!YNxmGANt9vom-Fi8B>@?;bYo25M7!#Q`){**+V(mWF+^=TUZ+}IiHeOaZ}_T zxv3P#|FU<9Y9qJL>yPvj8^3dt5N0}A)o{0A3yfuqm-O#C>**qRKI3^?XW0lvxA9!b zcy4a(4CT4x=M~wq_QU0+i^aD?>Xi+`)n0AVSf01jd*Ny}&PvW1VDuT+GxAX_oglgv z+{)P5mRa3LKH5mC*q?KvroQxg@}7#7#XApcsq5s|Lv|@WB%oKUA0Yg+iykZ zgs~Ehdl*YFOC1v2LwCo(wylkwq@hNK+R+^2n)u40q+!j8va&ixxy!S@tz72L?1(G~f$%FIATj@*PwPDUoD>i5VoO!C4-kEZW3iR!}-= zSp!zYW4Nt70^+lyKic(3%i<;*f0G;!{Hl33<-C=yxksMiez#LEN_|>X*4;p*9VNGUQw`$?ja$L718p@%U~zz@W+RBWx0I{{ zVTWY-a*$(zie`aw!^PaiV6oOZh7Q;cMIDPluNUcdO`vm*E@&-4w#OMaB-0Nc$f!<+ z8~uNlN5jkA(c%Oe;1D89fxcUy!Z;|`(3F!twVm9DQK%g1)BbMGcwuFi5u2PYY&pd; zjUA}(W{e+LTh)!J{X5E9QbgKwuXHy5 zf!mmJIoHkpmqd$IX`$h-VXQYi$y$ljYv&>xVIuIf_f4f~{>hHL|HIH(#YOSAQQYp% z*8rqJMClR~NeO9{kPuKrI<{kGcY3>n4Z1s3#KP|G?$*EE`f@uLb2Br~XMX27=X`7a zyyZwO5=qV6&+9!mf1h{?88lyzx4jWLSijCR-@WbZBs5f zdW}s@1HyFsqrZoYS`nk30HanWQdfAQNdcdL^t87}y2TfY?DiiQd1vq5{YEg9e4y8f zpBwYNy_mNyl+|b;9M>$aeuP`QkI`FTHY;*fF{p!8oCH9;O!o1v!&$UrN2V(m7k=1p zF{0p=?OrFHmt5S#6bHu)wCxr)g~l{I6_~FXSM9*7ai6FwBpg=olzMEhl|+0R-E8_l zo)uzBD>w{Q_!YM8Q>c5gy>@jd*Cji4KahQkVYMxmJPlpm5G;D>_pwSS`0PHl;u3%S zihAW3!ouo^*csbsx}D%72WfK;kF8u+n7wbNJ~-?9E(>jX((CR>l|Cl46;{j-olxH` zt@C?Q`BUuSKBpp3aAC!;g3O~?eG;|cex{AY6f}Z*au}~$R=8wHSam9^dq8VoB{g>K ztq6#j)AC6j6!Ns*N|EY!v$9x9ayQiui4-dy%D?cBTAPk&P4i6e;YM_r${e2C^t0gV z(Bk^#S)~J()!9k;U26snP0{Vp1R$~o@#+AxXzia&B(@X9(sc%C?8 zHWd%Stf(%BTHE&)X!q`E4$oTKf28hfl5gkDs`JqYTITAXhxF8iYZv>SGU}A;-CZ$<*?IYU}vo0N= zjKilLI7vB+Eoi$)rJ!EgS2SBhjLoLqf`61xrc2^Ox*@6A_l4Xqy4-3(DHhskMxb;2-jSz0ffrT!nsxzyl`)O3#<&|c zFjCPuKEq5ixZ3Uz^8xqo!-M5x^^?#2DgUaC9||DdQ1vy7$wq~@>JNo1{R%_W1o5Gg z2Q){aJl%snhQBqkhklK)^~q&4pdajpMnH7s50;i?*VE2smnT&J7~D?gb?ElV;IjEc(+7^0{WlO+ms392rIBqS8Cn;E zT}V*Ms%C8Iq^4GoBgWCf6c@ea^Z;>?-Cu?S5q+z!O0Y6zjT%End_qMw9RZ)URsPyM*>nI>CMt{Tt8WW_`((EBfOm zrTu3!`nt>TW2QC5xko!ZB*E_PzH;+6fuY zem4Ca@8R7t;7Zx`(=~vRG-^*XKqW6|ECKQ;GnMYZHmWr=3do{G7E1scJuPiIpkr){ zr~w97&ja4!4}kFi+v^VC1hkLrvr>SUH^Z36(A&pl%!laG{)?YpDa{q?Au(xs%uQGNu=Z|eV`yOZKb%JqJ{NK@sjQ>QL zeQwN!f`>H>rYD~&ImImJ`2fqAL&VgAFxFh`UP>~n1nmq{v(6wCuLbO7u!n6lyNG-7 zY6bnFcHR+dhM8)xr;-t`D60C$pvcyW%NhG6eyo4Yd7}0C(M+l!Ia$ek#LEqv##)aT zdY)!Ap+&ZvSbt%kE92U3$os_NLLV^8($WHs82~o>ti_oJa4| z+cDlT7HVpDRxtQV>!e`jcxmnWRVMvp>2fmYF;#rsM7)D%TuOy5|$D%X^L(l|MBB_mjKC6vn8FPC{v zWqOOY+BPr^_=|G~sK<|u-bbOnKDe;Wi}t_4mAY}XguW2o99nBf4lR~Gxv6{yi%zS_ z-#keFq~8$g%qUcud%R+NkxaGiVQwP^&vsK4=UxwSsJ$nmS~pYg9%`#_qb=S$f!Itd z>Wim#&`z}jIlt)M4aDYDdUqu~ki*|3!y;f6+Vf3mbG?-pPtGZ%1z`>XFDfO%^)2uvaQW;c=q`MBhywY;Nv$WiebDua7u>*+E%zeMV`w5RlS6Qi??~oEa?#Ce zIi}p8kd5GL&TscNu!~b`eILx{gq@D&o)@Bn*SP@#|CV6RG2TBd&e=vB#UFC!<9n%b z;2G?24hgJ6&u$u7+#*+kFM?_CW%q2*6*^}92>8pbJ;~&}Rz>V_;E)unoAz_2$UHO_ zV3))ns|NE#EQ%lKC^)rU3{K-O*?0~3PV5NY4BW$7-R*$A$P4Sw00I3w{*JTW_;Qy4 ze5haC=nJy7iK;`OwW<}}4m?#jlO2F|>EUgifLxrs(Gh?JZ-QO{1-x>%??43h(`F7Z z2Z=vc0N!k*54eLnYrodN2gg-0lp>(jFbhclinP~BYXCo0WAry0j84ZAU-fv zxWMf^`z7(*#(~|ALPw{7XxHSv5t3Kyow_}Mv9U{01O(LLa6B-vQe2+SzOK_}d$NzJ zTNCE8_shG3T-hDs4!0(@ls{;*j=c$!9Xc(NO6Vhzr+0$`wxF4K-$gZai!1kW5(Ew8h zHp&XwbzKLz2s^Viu=E_;p?+WHf9!=-%i_)0b1Dh~mF$U%Znt#SZ}AkHt*nt8_Mr>F zYTeklBdm*tyuf7EN!1m% zVAfGdrcD8>ns|SNEF38L-ZvodD~qnXz`I{A8%gZ7lX_t(t|Y%K7h*VdMYbm@r=`T7 zLR#r?APyg4^tu*6PndVDJh;Ev-otc30==^5Hh(H(QSCzF0t1&X!rK|IpcPm%v!E;) z?PQI~@UiySfLz{ zOWubNQS!4L_PD2PjBzivQsOIiM14eTpc~mP_*A?R*72zs1JG0ATkIUj6E9lV#f8zQ zE^5w2B*UtfGXXj}GzA~h{cY>Qy0w5I2JKK!6h17&&xTn8>3PfXL zXxw4`>2*=u*@VcYgp-18u{sLk$c(`{JiN}PH4R%=^<6(7^)p-$97cjFdRRi3q#iD0 zLOqIG>F&^Y>Bs2L+*I*c|6iOo!EBcp@C)H$^$1*oy6#EDzP3fRSfJ0E=oN2}yLFBH z9{6F^HKrDxZCF*%0>x;BX*_P7@_BSQ=by~WzmXFo!dx7|3f^0*U%&y(Yqu?SppVk@ z6|L@?p*@2LTlKsKSkP$7Xn_W64(H$Eelc!Kz0VETevA6RX;9MrW5FMipDvTY^#Zx| zOrQi`GVl+Bhvbbf(9B&L%@rh~2O|!^!R;yZ+fZ>+P`)jHi7Z8He*%@y(cpsb)kyYg*J8+>O<=)&xDuj&#aLcCf!$+=hMv zNBg2>)f`>@3&{yisXAFao)d%%g;|_gv`hT^oN;+xyiCrNEgSJ~oJkvYVhYaKHTzI! z&RC~#_%3IPgCghq2Sx(g(ar6sj1#SY>RipxTdr~r-RzY#p8T;_p% zVJ?d>;{_+b#?IjXMW&!^OtEGT;)t$xngufMUa@3xe!@392F0JM zK2#}0p++-Vrl3j}0WaW>){sc|3AOTc&P9B++&)Q$zLfwGX-LfoubTo#^DS51<^I7X zmLow=1W?{F~n#9kjNU?f^Ll-Q$ z=C=a+C@fz!%&p`(TMluo&}VIsxTq`Fm?Z3M%a(ZXzc7wpz@i%DDL$@>;hq>@QreI_WEcHcIcVg18fei!EP(LcFwPyLjq>U3!Wz zaF8oP_)xzK$RjRw?knAmr?y0GTa13MySO<4X|J>nYk+gL+k7`di{*P(<#Eo6f-Jv- zn}|uR`}Dab3yh1jn$l`Xkm`H+WUjfQnzXunt}LGnZeK15pj_JgPGnD;8`dOPLf^Q$ za-<`w-tip1k~PuX60haw0dMmYQ<%P+ zorHPJmtm&-9W3?gXd;gtj(;)FMjG$rBVD<-QtG4{xU zC;)xHzAT%EE-P6snSlthnnkmbNgLk?mci8Z?mRc>iO&f<97=YqM9aC4%+JFFmsI~v zBNj6?!>X~OBz}(EC^TW6m6q`L6$gn+dHl@BLKd-oV-#P3yM-PmI?&%fBXumuSw|W2 z9`3go1^tC8>rQLNs!ohFQ6S2xymfLLxjXZ)eL3KiHfN-)eRDXvVGb$jJpy-Rb40%5sFn?O9X1^ z(F7Y_w>UQB9eze|-De8=khk5D4*$aESfp@2BZq20b##5vNN(zMO+Fqj%dU*2|B@`% z&n-A9ve*2{I3e&*zKu8VVrAz;aJ)d=;(Znc1tP~>SVPDy*jxcxQw^)h!P|1G>?c$2Y+f49Oleme18l^L=GpD%;GQPfV<=@eh z7@Us@)LQTPNVP1{(E%C~Mq8ZV^x(3}=e3_pmdQ=4^rdpt#gJe2mNu<|O0viwt*$2* zrdud(Q%=Nwk}abx2s$J#qUU&C6P#w88j+>qSpjC(uq*8UjEyyCsdDM3N+~S>S*j<~ zUQ(xMMRX|7M0Jwkla`^F#vC1+B28r;3`!F1VsSld1dG^l4y%Y-zg%p6=}NUgpA z9F^=fRsr|nQ93E`nzC3k06f~cLHQ3Ho!TLf0bOG*O4`BHz+a+SAmLfYr-6qYoQVmX z>B}~w5T{hXs_H9xMO<$5;pDvlyI$^Pp9kkz?jWiWg!b*iimPLGL`Oo!eEHVEku z-7}o`8k+0ihBLUwmhq7vT%zKARk-k#2s2mu)i=mjaHebPrOa}c>a4hT z$9RREXkN;GsYn1uM~eRO?gsoPC?VWEUK0;6t%C=ag)Uw87d{I&YhF|)=$Zsdy^ppC zbk};Rt;_bPQWe8F$@1MYUdk$|nItvpwn!>k7Emo1$G`7!h$tpn9X!!Dn8th+9Dyj+ zuPaB_aQQ8|$CZ7+7R_D#<ZA%xTq)>ZJn$tyi9@vY(#hXu$@eu23La>H7cfN{&fZDPI7vnP`^Ut`sN zy7+-|RrAd3n=)G6mZX{D%ayeo%!Pp!ht_T8?NVNL|BhQoM>$MIDg;^Po!oD@wSv{! zP!f+b8!wk?nR#{d%Wf9?RnbVZvt9Im$=QjCTAXq`(oJPSv+$oQJ4%P#0>u=@H@iVW zI*VhfCxU_1a#jmW4aH;)N2pI3%G&?Y@FG$rL3hmp^-JhKHxFxi7#AZBD>pN1)}ED( zVNu_t91D^Ee8ZRKIu*cX5#AV*nMFQAn5A}b* z`G{miB{+MnKpGEjcAGcyZnxS!;r-;yGjqglbJS8=Q$KPOX{eu!q|+`}=fh75ejA>{ zIhno{VtD4JF10^&H2kal2UM~)Mlt{`bsHt5ac|gt6p4DELlBrHrzT(^YsD3~cknuyCEx5F?R<)R45N;>CO?+E(Qf$CkuEz!G z*j)ShgfDX1>=im0R*DWcPSBE|rL~#r4HS0eaivw>Q@y|JS4N@ct>o86XJwsuQrHFA zcHx*cBC$R1yK5W&1^&<80^f((FS9^Eq(Jzzp|$cVH?DS^!J2%ma(hKd-df!)_3re$ z8dzzcut(`Fs|zcU{*g>x6CpY%+~LaOvv@b{ZSg3)XjvRG9kmylG!!>@bI_WDwKK`1 zD*sn`bEFQ6t5>D_YwWaT3GRxUs?A{&rFyxk-yc!B_>OBDf2p9=-V%F5R4t>!t>_uS zrUth*F?gy5ZhAzDHQuPV$$g{qsot2@ufA>2#~)PS+Q;jgrD3W`e!E4}WfrdPyi=lB zdsD1}*RrevPQl{&#`>>4N5SPaQ61d!BgXm`m)!h{fAtsBg4OzJW_+e1#9*^NRXS1A z;s**(DXd-R@i6f|`^8uypSP?JUV;r1VZFH}_t^6~E|sk;z235|ydVc_@FFR4dDNaozbNi}IwcsIV zjj0K-h_e@)*|iOcW{z(C4Sz5G(%1{Dw!NsWge_A>RklJ`V-j?OkUE&Jz5>PhJd?kN zW;xYMrf_fD91yJI?lVmrnb=)uY-c^cg%Qyjz&lbL)$o_Nwk@IN6@E9FYy62%ihi$~ zj;#w0P&=Z9KKtZRh~CL4&Vz5-+~ZfmuBP?a6{r&F@0=nXq}Q}`NTQ2s8uCSVvIlE6 z2~(5)8Mg~2L?ab6-cZn2RRyulM=INkPjga=uA;*>zxksOmFatI1)*~8Jvh=L(q5gVT9mZNP$D0QI;(}H=|M%xM$uOvf9Xj9-w6~x z;5pka=Z(gGnq{F@$Y-dg{b_9}wWpa?T3YqDLX@aCOjcJ%X+{w>y=$G9 zf4ffCI4aYfei}1s^5D^G9;5ag$gc<#I}L>Pu@YZ3y3Y2 z!!FyKTv}K5VPJ9D(_BjTB+~r!F>Pze4hhE^#gu~Z3pJ0ZC;bl^SJSOMu4!cq)G}O; zp#B`74bQG#3B2~F>P29X$B_CNSnpUQ_v1{l;)^;s1t!aRUYxtExw}^&<4Sva7sG=) zQae_`S!wf{Vdz`@zq;2@SGaRk6h!mitQ&#?J-XDk&}>JBOu&6<)h8OmJ!euvNVvmH z!>&Z$?^4g6NZyK^r|sK`nADc0efakHraBv}GVEO?AARP(LN^Mv^QcmlBL$AQ^gg`b z>VYsGwl>+1KN|5FE+1%-RF?F0*NbCw(%KFSzo$AiEfR?118TYasIY3|bKcUmzbaDk zM;=1uC9K&|DfK~F)-wgo$PUw)xC>muFb`UCb7bz0Aziji6F}56Q5WHielOZb%%~)@x&gpFHvuNBMgln? z^5719hCC1L98*vc~^f|K3ad>kSTXxOVQktP&_s&qJ+B~Tg3zX>DDED zTO2XnkM^Mv^r?OSHEbw4+9oxn6f@EuicalgneTJr@duhx*U28*8VyCq6__mOi5w~4RSpDUysj;oqzWNR8 zbMM?N*S6 zH#zpsBp;1@*>0qc3jEfzoR+kDVoe`?mrInvjrl+Ozv?a)#bT1ogS~$VPw*L>RMs@S zh4v*^GxVMAlYU?qLDz0_?3v9liG0!4%D5T$y>TtGXZ7^z<1DpHhJGCzUT&!x1oAC5 zNY{hDOWp})a%Pt0A94X|b9e5&36!T-4N!sfO=jJ5fWpYft^GiI;J1b-aNO#-Rd>Oi zF2%YG@W%2$<$X?|1tB4G3`;}!A)LQuYYv(s5xF6Qhv43Hq(2jO+BBtWGIS>LT1zJ+ z2>e{X9!g!ku<{M$;lirmK?{~=C{4LPE%u4`aUU#|^Hki+W#bR5=UvO4u;(6eBt58) zj-S|gvC|fN8`;`?9&-=8S(l6QSI?|mfIM;`w1<$j%Q=d4xX$9ccr^UKrO%1`P<3g; zez|1qj`}?|B39ay-h;xa8|fYS{5_G|o9FW+fvvS&1jXl=<}cm7RLH8#34>l(sF={)$a%sNC(?eK-h#d?E_6%ODLxO1+3Ez+yDA9XAKt61GjDGim`RPQg#mAFWjmPhR=Q!f%-E_0M~VCC>tJ&*X!Lil(+#bw(HGHLo!|E~%}1A*e5XQnjBUF1OVycczo_G{s3? zwu=ACwuhdHm0p});JnxG09M#AVA3(Ccudzn{jL(v)|Y&BDYfAn zy|!$qW^`^fX|!QNVi!3?{U`JtMJRjfIkEv1Ub8(&H^<56zvn+cvVDJX;imn<_R&Rc zyL(4E)$a7%=CzfYw42ecmu+p_xZ_`WLp43om-I%*3vHr!DhW>ys#+|yJwtmjSXarqc@uXP3r=ECUa=CXd&82K%TmBA4c}jib=KG`?C&K zZY4crJ)!xoPhp4AD?A>s+ZZ=(rU53bz>98lKdR5cIJ%blsWXcHnO19@!-%2>3+^-8 z>0cT9nKK#PJF}PwGduA+YdmXqXa@^n4SIm=QS9|LGuROO)%jUeBS*GBo@UBPYiH8* zoRxYieLiO#pGud3AL%^CEbvWkI-?qVl4#9b4c-i$%e)Mp@L0~u2CHpmvVMZpvkNFm zJlPOP`AP^{FH?EAyy6dS2G+)Npc&D7w0ycN`oA4T^qa`W&D$9$EDqr@ouPy7_n0@i z4{T<$IGmTKL&$M*$>2%KSm|(!H>E)`UdyApi_-DO)XRcP)L0sgADIKwR}!Z;b<$ts zAt6&4_2_`Rk(q=nwJ~Q+hLTQJlX&{FJ#OR>?VYAU@+Wn&W;unY_=Yu8U1gn=b=1d_ zo!e_^wW8&l#?tfn&x19Lm4w=TC*uVcXyeP&!23>EmcOkJ-gTbTQ2n8?kep!bRQ(`- z(h;bJQm}7=;3u(W^+k@Td#{_ZiehicdvPociBEOFvD=+StIRKI@ zS|&EQlFl|vQ(hw%*VrNoik&fuR7QDTfo`*>?pEzdctVrPo&?>dXN$+W&tt6MudvBs zJi-2V6e*uNaIt?~`9P0#-5*j~J6kb89^3Q?mXU|*63XQiM&;h@a%z;$Bq5!)TvZZ; z(Z@>r-QLk}3jWzp8I|~)BN=7=2arD7@-2g&wI4~N`<}}8lNviEP%k;7#kcGd#jNgH z)(y(X%Cz`*)Qj3nLH@K(g@@Z&x?EIagEF?^&ktvn<)5(a^(&t>yrOn-dH;}&>va>!Bx0Cfi6Eo`mZ7c<#-D`@4d< zaYdbtp`80A>8ckTLFpZo&j~KiAg|+$CB5C&3_hgfCHw}@Q6B}BgZt@;Zd1T6M!yva zY-X)HYQdRK{?NY@9H5-42f-q$Tp0=m&_a+!;1c?5(pcad{Z)1~@R4yK!2$Tilm=gq^z`@!RfETb!ehjbxx}lT6T;N#QV_-7yBC8u% z0?vpp8sT5L24(=sV2mpQl!NhBb$|d&JruKla(wT-Q~;LEJmx2u@Xg- zrL$QX0s&_`tCa6va-BsGuQErmhVa@rbJlMxF@VMPM#s7?VHd$wR+rd%$YTFuCSObH z82Lk2udYmBwkaow4>9-1u7SSHE0VLt*O*^KXSc3oxe6}Ea#<|ig@6UDL--Mw|5!iK zt`SC}6-*y$Wqhc9(w@s4YkX&%$DF78D>5>zwF>|r=6Y2^(KlwPT$4d%4oV)z{9w%x zS+Cp2O5&%xoMw&43f9wDLrDAH1O~MwqRpCtG&Ja+G8DCAg*c<3avM94aZdN5$b#`( zQ<#2=8Ls#iLuS@Xsq1zyzX{*EG_t&TJFG2O+30~mGsc3Rs+P};WgR)X1B?|dRsssc zxBeO{ijh@aT^P-f7;@507!S2J(O;PUitFpo{f`VX=1}QCqOC%^#N+%`tdks^a6q`{APTnGCpP&^oWZ4tDrFYOy@xEAVXyFp6kH!-xG}eCS7g8F%$9yodJ!Y^b2+%JSv0b z3*Vuo6gt4}^suzkPz}Q=dJD9Kxxn9?t786fUda6)>zbt*N69|Fi-QW-ZcW#a%j}Gb zsfaln;ZK3-Y$J0j^n`u6z#57N=BIw)UI#Ws?cr_*)N6NhJ_A>r-g8iJoMjkiHW)B6 z98yqn<2rah@>Wv?TOn!02}l4Rp?`oTz)|^bTm*WW>dZBPs8P0@gV4maDV$8M-03W5 z9Cx8*A!y)e`b^nhL+G@F|52sS~ zfO3}Os>sacg6E|#Q$~TXxG)k10|jr_!~x%VxlTJrIuqVoUIAQ?tzDD3?G4S<7r4%~ zC2|3$yJ{uk##yd^Kz;@u(su6H3udS|$*JICc~sF8r(`9Z_=XR#(}+sP?*=Z8HK`>-Bj~#-~ZmZ$E+8<=>=Opb3mJEO=`fRvb(7WSKxe_?l zOl;o|WY?J|?g7kqkM%B zFHVH73l^8WBV)Y7W&9m+L>XyS(lOkJJRD(*&7j8m9ztHwUautJD-6P%4DDlWY2P4r zEB{kjEj&OTFFh#OO#TT8c>gF@NLz_v>hYXSxR7=#@duVmKN2oQqZs>qa}W>azLg}{ ziM7{!H)O%S*V-+-$GmM^D8N`wl1=>8EGjpi_|57pkHxRD$868X`q^6({m^>$zVOM2 z0a)bwKUf7&R%Ss4;F9@At_ED!GEZ?h>AO^QC_ z{@N@V$?bN9iQpgH%+;6RiQEM%6QISM(-zj;WgM&t<9`-#bgsO9J|f&p06Z2r9^Xi8 zD~-g0@UrYabTuZ}bPtI{bzw6`2v~KiQ3!xLR{C?*(0+?}j)8lo(VZWrSg3u>TOeZ! zsQ3%XRrXWtpxC4&8XXYgS+|fr{_jnz;M2VA>xIxO{M2eMXe_pJr5$%6Vzgj#=EIZf zKlA!@y_$Z4uPx$l#k17rtk;;Y;&5>p>Lo+7Oc6gx^hPzDESkCA6k_sE`+VlA2z=!{ zPBXUI!U$F)7InvXLA3#D1z}RPm6w2hF_bdDqvtA0ij2q^H9vC)d_~c?aV+#nx+k;) z`k&~qPdnF@f5Gt=Clvo;F$Atbm(;cpe_94q4E$_k91)5cYp*fKp=DKGg~yO0ee2e4 zn5B4@JZ~SZiC-T*hSl$|Hk=HL= zN?+jc02{?PwQL$v$m*@WtzwsFDXkRYBqi=4^CCCX-${}vvVwk*lq$=p5nP~E$NTe_ z(VK&B5i~}R*AO-WYjSvi#;_hN+YAo_%c>HUYZ&L{2jtewZRi)N1M?2;jyR2Foqs~u z#Nwyl;D2Y2imTxzvx|cZ@T2TUUNGhgq&Pf4`hZ&_y8Q~U*!W4|3AxKiG6%>JIVJIf zCQz@67~Gz`CxVMyAblam645rL8R?s#P4+W4lRr=LY0Fmv5q*qF z#it8i2Hrq*yt|&w$TIwz!%3(HePW)?oeux3@RDAxG8cXo-!wFFjKUwfIV4kopC)cc z9j{5*x@8lwL_RV4HdZf53Z$TJ!aC1Fc#!waVGvr4XPM)iTI9agQ_5}h6MPYme7InP zu&R1t`C9%1!*I?+URVVyr3>Gyc8iX~Lgd#2rz6)SMV@x>7U2wsI_?AFlzBZT9Bt5e zOFcV61e3*nExUp9!o#^s{=b!?^VX-@8;jtmljHZUTsftR9UFnyyWTjSBj=G zesZ78gX9}!C%#F3uudbyshE2LKajrGK9R6wPBtCH#Ou3IM9~xHFl`k@Vo7Vu8Y&b1IuJeaqREI3RA|^h7oaX`DL$27V$( z<@Sy6;>hfUm;; zJF6I$PcK|3+a|Tl%pK`q-?Ztq@S9K^5x~FAKe)D+I6}O0<6sTAwY?GHqvgv2psgd^ zn9tgDLkP@LU8wj+{jP}D)D$F1KdWrE9ue=A4{yp4@}!yJm-)q_uWPf2WC7|HhxziB z*=rFq?CP>S?gyknJXt%l_8lZs(W{qJ7s|gI!}29kxo+Lo1>!_a=*DwGTV+Ie3V*uH zXYB<1huF;Rf9P$&EPDZbfG}GoK4XEI!XE@`KSec%l$W7qWI4N}7OF-iz8*|Xu1%+a!DZZP^u7_HgSJ%u|?`Lf-d+$%3? zHpz>Tj;i~TF|5nt?7#D{{%^_6^mOfmGBm+nb%GSI-Y7pt{^++ra*^8T{8;#sUS#u+ zzk(TQ!pAA>P5h(vMA_ANxIkM|I+qq9#H4RS1GU40@kmVJ*MaT z-WUI1=$&s17c=kJ+VC>jD@k%Qf_lY&Pq%Z>!jYO>@2?*a|qtO4|syuTbs+7!yc^i<4iAWH<)t9 z>>w)UfbUaD>LuW>IJv@>vmlfyjpo?;(nK(4wewm2K2C~l4l#=(HszvOoNM^;8a~#- zx>#9=%`3g8PePeFXSH$2gDsa;nMh{bbU6wB5V}~>0GIi$5k7{#I1lm7A;=cMOCdYc zLCL(6gb!>8Vq#}dqZ1k7yElJV2OJ{EfHICw~DSgUN;ptd2**3wN zkZiGm*R=Y$;1)5!nO)!j{yGU&>;Fh z7_IVd_({8Du&#Ytw4!20)ljxX_0>R4o}!ph;T64K>ZJM<94ac53w%EFyCqAUXXCE~ z`)w;w2ZCqT3+19U(DBL}?f;`W>Tfn@6dJT=>Z`J6t2(PMCmH0&4F96DB`+#0gZ_$a zRi-{e{5;t#r}ub+$lO+myx`@Soq_DIxtxC;+=5~XsU@w*BL8v2*5V_X>uQXpQJW7L z|10~tfvO83bq0J^cT=*wxC$$p$;uLmj{ekYK)9UQzm!CrW!G?4x7(DxBA;!xCQZ(d zsShTbZXK+aQ!F<}7(P>jHY}>xM56@ss!q_`ymI7g8P8X;#DgqHtG9w^Hf8B;{3~!B zEN>e{-%Os-G>`r|udXhTQNDFWm4WeQ(>22+CK|a{Tgq|`C|AB?J@Sf_6|uD|G0{gL z(b|MB1ech&;XdGhKwawuP)LetGy&^({-})uyG8`wez18{iryML6q%%vgU~g;TY?S7(_Ii zzJs06{STT`}m%4qKPjS0PQai?aGn7-`UjqN!-8Hq){u%-rQ8XQ}89K zywacF5G>Kn;${0bslF4Yu0Lguu-guMMZ;*V=8}*O#(1;-{DV}UNV_YFK1kb3LEXnYVQ+^XIbiE{f%s=mNSa^)k zSj{KeFowy~k*=Pa(%l^osxIbwx0D#Y(l0j5)xn$K>Z_X04WMDS@=eestzPcrYo`=S zGF|sc8A7GQurP!7(8`vG#5b5uMY7QOrCuGB`cFG9wal*RPiNKdsw~~)Q(dmN+Yn$# z*1QU;(FQ4dd?zYBW!qd^rM6-dheN^{{I6CX_&@x&sXek3EzQ5OKeZ@5D`QVeu^`!| zFSg`LtWT$Z*}C=e7Ps>I{=W4V&Z|f2`alL1QEj+`2UbPi;(eta`7g9U@Pn`&5+I7haAXoElApuQUYKnj0 zzRLr5|Hpfgxw>x-@hoX&r!)Q~Ca+~9ZW8*hemj=6_P?qebhqbg9S5D^WUn$JJM3;r z`{0x2|L5wg!kb9jFpRS}EV4k|g}P98rwVm%saul9WhTjZ+>^R1Erpiiy12WuI4sU$ zi_88!{txH6=9;5#4!-w&p8I|~F}?9HrixZ0TWWlEu2Q}%{xF`SsLOi336W1pao(^& zx-I5>?|z9aZd1f9n8^Oiy`;$RVPD~?Tw1lKF6E($&S0l=1KjnQ>5aQTY|1imgNwql#72@HcAc=dra2i5!kh=&TU`1hl+NN8Ff@;zTfE68k(Fw zu(l~6#(CPzG=Fb*h-jZs*c=ut;)O`h9j4?;CR#r@Fd_<-;w^j+%H*jQPQ7 z16Hrz%)JQCnE1jH@Wln^y7zqZ1GC5zScQ?-lz;gVt+%Kl{95Trnv~zgJww06|EsEJ zqWL#Ji^>88K`{mFi-NM%8C){~V7rvZ5vb33)&>(b`#kF2;cXiO$s+82OD5$L7A#>= zi_kI7M%qbqRpl0j57M1E#vDYJMqgzA0rOY?#t}d_ZI^N1K*nc+tG}qm_LSG|Q~o~G zM5>Y#O@EQ!%8bPJ^%Tizww(G#3{@PVQ$@ZRpBSI;7ttv!4yN%w&i)s5v9;w=;qFti z>V-|mb{(m?*O0$qeqDopP9v4(3iUZ~MXt!kBGGZAulK!aM%&#J; zw}vgiYiuL9)6r!o-d4qRU)`BdW85(~u(S3~tE$15By4KL&r?hc2bg&MF731O6|{8q zAL(QCw~EjxHd8Du@GfJo6bWr}IRjYPaj0@|_{R3n)trI1{XlJC?{dSEx+k4E*eSBS zbr++z-m__WSuyp7o|kS%2h`W2Oqpg1Tkjy&ehJT(!pX!{$6_j-x9-_?r0UbCb6;A` z*5Q8L&$_~aRp?pr?Cz8Flay`kg{7m^qNWdN{q#9{PUJbpPStDg6|8DmkuA(wOf(z~ ztk}J;Ve9@XY-e_FW=;6GiEdu)tC1(jM^f|PKKeXLY!6YoqW(*JM4BIMsPR>#gi)l0 zy}q$56-#W@?As#Nk)n#EV^!nNt1SO4>ycFdy}MMKP^;Kli$s%xH!*1p@;`$$B^~uW z-EpZ8XgRHmBGVYl^=G^uFrO(Uwyo^VVz-ml?2@WIJKwNR)m$HJ<^b-UAr37Dr73&alsURe&(J(ptw)06dP*XL{1yc1&a5q-XSB zV<8Ntp_=W%l;aE74_Lj7a~v)E&$2XbCg)n(d7drzS!50GGw*|!5pW#1Z#@Y1flrRS zr>6s@+vYRuf!@A(j32-Y-CgEIa5cJ<)dMQ&!)yllwsbuwhrc#$J~x&>8u5s?S}?=& zI$$aYvEB_X6bKHzrv5eI*KwttMtAfS(08MLS`uRrxrFeTT7*ICUrcXU-46B*d`0*d&KvZTXF2y5a?tuQumG+r*i)NnycXoCYQh71c4#wRhLfWEH3+ zVRzXExr4`B&LQy$8z1f?JYdgS()Cf%@MrRijpWw;`bmR%@&c-HZyMi%7S@?Wo=&fB z4JT)S`VT88Eq!=^h4GCfKtQ(q8kIft_xra?Iw^+1bS2StLL1Jh& zdHLRX%{M9T+f~vn^+{ukfG#R~7_Qqz>+AcgU_1R~r%CcrW^i*{*b)Na2Jy|hdVmd&kN&OwCvysd1{%4cLta| zK_2-7;FB_dQgC{~SKutDPF8|Z6Z5?sa4)~pJ&o_e|IezF-!G`yG0y2i5;siYd`BiV zujXdMTcsh~-Ebuk&NGHR>hgF(cxHhM?-z78=@TG@wuF2J|A0XERbV^hWQFqWgx9yd zXG79+17+;jlGBZUa&pBd#P2v~MMrri+%V!!Z3%Y={xiRX7mJ%G<@26m!67;TLD#xZ z1OGr$tOmeAIAm)f3)lVD_l!MTliEwaUV$E1wZ6z#qDkf0S_X|>N#)}y)@Rw^y{tf$!2ct%F|D0t!|IO zb*w`zo@_gIW}{ctE%sZzckVjQ290mRIW9@*8LZ_w$}HWwd6&cst)~Jt*wfKc#*vMZ zZhNNB;A8DW=C(dBOw5YtVzK;Lx7v_DC|p^sVJbw)4@ zEn1@xj6%}`)q-*1zb!C~NV`)NuHaJr>(f^>AF z;a9p8oU9{a&A`>_DFL&<5;@By8pOq4EI)!eoZl-3nnv>*(gDFnox}*J8yp3$02RHx zWC=j-)T}QBc&(K5nLuBof6Ol6FYSB(Bj8+RpNk{tFRirt4NN5>dgMUTj-LiD;609t za{;SO9{?M`cqp9o7O?JP7e54CJDby5ft;4^m~ud4X!Mr@$J8p9CBRQvm(_C6lz7q~ zAfs0XHLa3bS2Lt9#BXZ#{9Mr^lA3aYcu0|yq~K4e;&d)Ph0cu8V=l~Mzkkp|cA67_ zbaNxkqu}R2Rd1K1k5bjJRV=Q*DKUs(nm6c3G|;K!-S{5Hq4gEmW0rZ^L2MGC0MTB!Yct3CwH=MM6g4OVEu>^h1 z8%tY)1_8Cv&4?bf_p5~8fPXtN;WU1O`7LOhprDIFJQDn?+m8P?VUoFn+Y1+RFJR@u z`E}#yW}#KlIPyano=QT>g*nly;5$OHpF5l?M4jl+38B{F56DM2+;JPfN*J`!_%vcZ zuEBzFE@uQ4V^?dpBX_Yig?A7i^u)Sj@HiS6wG9qJhJ4>b6a8(cDkvDnEaHV@&;(Hc zpR9PN{s#+@-^TJ#Oty{PjNFvcYmOoQk~M`3;7+maI&*kp0u~hnZNrcG4njeg#wk;{ z8!fS53hfc^HV-VpP_H_TYIUP%GV)OKfVB|`R{g5}2oEXU3JPE&dHGrmH2D8U^yy2b z+Q_R=uy~JeweS@2*eOOBi?6h35o|@*wJt(;x7}85M}KPpkOU;ZX&&ZG;U1auVvT!TBemI0qhxYaVTj>C~bW8K52z%QXB)1Dao0t(v zf-}0aK4pSpmFuck{O{7O7C-nxg5UgAE390t_)E>NhG2Li9F>8HqKDxu)JmN zihj16&{4jWGmmyfx`T7Df+4Bk;<>(JBVN`7Z~q}+7w&?O17E#=U<7!X@?>AE; zaRRNOPwBuHN*>7({$2iO*<^t^^@L=rz`tB6CJSnExFTo4V3IX4S8yln8um@N$omHR zM7YM$6uBZq&91@+glF}M3QO#WxJ#zMMBqiKG3H)>O#BzRrEFBxg(hSlAgYl!i8?$B z5r;9c48+WP2u+4}Ia(m$aJKnE*ae=fn<3AX{VAeKFGzm@`z4jqm6R)DXUWXcyTpIu z+1YdP%c8}JGqIDzlCa;=)A&Mf8gdj{=x7XYM^~6fK&^hZS1CMdBS4!mi%VudgPJR;5`q1AkKE23tb}I z%(cR;*h7QLYF|s7MBZ%`^^jB*qshAjOy0HELE^e5T8G%uB&hdJbrj82v51Ck+ zd7Xg)X{Ezgh$hmTj|p?J$I5HcqCquEh#&WfId&qnJHKv_NNoSL-W+#p(PSLJmN%}5 zw?kLz(2xPdM)kl;1KY}D9iBqg;`8RGgo|*w;+=HL<^<%hSU=p&=8J*`Zq_X(ru952 z?!caQ+{{Qr&$sMca}K%OzzQjb?`ZA3)c`i9_%8v?W6NBb|ep(@4LE-nM z<;diMsj;6W7X;ZsT@&V-QI9TyEu3%n6uTs3nhv2Z!q<|Y`UccjxLjL_&S$KcFqU4f zdadLkV8Q+gK5%I29_dMBUaU~E621_WB?4fz2aex_bL_rgKCrXtYeWmr7r)dUm)sUC z(+o;n=uWCWaZQz(;*3aIFjw}I*q>@Gb;ZBL&J`25L(nYZC6?_`gvX*v`$=dwa?#8d z{)||NUg|8=T)wMjhAN0wr20+qva(j;Dd*->Wp%QZ>v|*`B>Q3p#52UXfk%lt;)_Qp zb{cQ7pKwcINoJWa62%dyKC#Gq=+XX{D*ZF^Z%(|S^Kj@U3r=}rEu{XpFlHA|gCKj?3y zxWGK^Dwd_NpW7sh-|(7## z4GY`ep#S2!uN#umssSQLhK-}+d5!m+}BiwtU~vK!_D0- z6xx=Cf1Apx4(c--VhXQnuIlHfKU0x4mlIqR^HsX=Xz3Y6sINe*k^b#!MI?zEZEdib zM49P0d>ktSt~9%JQK>4{QGmdnYBS=lceU zeH4pbzTzJzS`6msn5f3|GEB$b1GAfs43MeO4Q{=GmAJ06YevBl&9b($=~IJ+5-$SJn6l0N0(MzH7$c$o8fa5}k^ z;?`SAd0JlB`I=gs$7vg$SBXg{5yyGORC--G*8=F`c&_w5;u}2NDTy==4)@LFsrB%5C zd!0yfd+@eZkHnq-%{ZM%7h1E|^{IHLNQ2#rfaKEu+IIrSbCX+|K$o?<8=2sy*!B7( zzJKroO(OrOw@{fUSmy-GDh1E1u8RR7YCMb&Lk=vow-I_<_oB-JdR3a%b`iRtv$uH{ zbZTvALmPB5cCrqHE(dq13Fx|anW7WA?If3OgI-uq5uJu78-K%oz&6b5J$s1jbzz-Z zL}tm3wk7!a9Ixg{ctFZo!)t7JOuzOT79PA_^%y=zZADf{wMi(J zj4Wa<=vg4w)V6hu%34cYTglR<>;p{&5fjP!0m>s#}s!&rTBeMie*s_g8@##4%uDbw^vykT{+i|&kWG&);-O=BBWt@x~jy*9{J%5FGiiM=F|)>U{saoOZM0%DtK zX`MfMu2tK&?dW_}Jhd5W`;qymp}b{U(j6VS;aBu>wLteS$VbtuKInx>PbuV1(V{0( zt~CjtENU=ahWO%tP&&sPs-zVW8&A}z^8XubtP`bO>m4K4#NX^ZS5JyCZGBF|`~#Xy z813#{eI{$KgQZ%=xocr5Kgj#A(jal<&!tS+JVfp+KRryUH_GoC$fYXMkRFuwG2YOz znZXVJr{xA~k-tsjLiV^jriiiMADTJ z8(wMI&JDA9AM-Z#EdzL|ySws$w`)GP89*SM-*k+>)bEYq5B|8jLNj3tbC}qf3BFr= zlR669jV=-Uph!~SMkmOl%z1Dr6q)DMYYQc%uI;2j@oRcow?KK}p^Z-=j^8h; zo7U)!_WEs{K!L})i&Wo`e;m%pbCG7tGVxwyjd4Amg3PWnA5u!clzR6orS`e`-D8rt zb&VY}#hGj7wxo&JVHX;ji5@?N_B(#g9Z{8FzZ~w%j$odaO0gA6G2Vi0Lr&IeHat^% zlwLfp@Os!9%YN@% z-mr9X4ml zeuI7*6%oH+zfy4A0lSs^eA9j`B~><*f!>Ix_I*Kth_Nmn>K&NS`VaEW>un>59Cp5> zTa2jeOq4@Nf%#798pP3Pj0i)fl?HB!mz>WX9(gZ5mAY<2gXngAXkUnEeuT916p>`!Fq36y2nQ)kS8GXV#k@1p$$6#fDZsmxF!Zmf-;2qh8 z_<6k|sWzgd(?miEG;W#5?0G$H@E1iof6!bbJnc#qG~Cg`NU{>MHFldw;Ki4;jb72^ z=C0hx)e!404VtTtuDRNCT4@rox`QN_1blCqE}P|H-*HeA%#Zone5d6Rhd&vD1(v7PLIjFDCt*&kO zN|o1+6FIb#q+85$jM+QM{%HNr5Ce8>5XW$v|>Tf_cj&E`Gkem<_MIZh7R z;a;bv9Pd9x_NPW0GAN_8E!Zon6=M;jnPyKvn841J?m<&LQHK&T?>?eWRr6tXIg!|5wx%* zgL+T+Hr0pTCp;Mu!K@ZGdDgLQh3VEB&MV=RgFyLw$$w*wl_lcOUHhvgqA%)KwPevO z*q9VgtfH1uZ1I%!T#+$_DK{W?0WB0*!^ z8Cs=QrK+?wVnx63RUJn@x4wv6DWw+=)MtoquYE&{6a|OZFkErH=MvTu^s)7I_7`N% ze*cnZttU5W%1fI)+CNo}Hw-B&tM}>s1dnU~)ZC^BNC#DdqPLX&3Y)d%)ZNl!;Y;aT z#RVS!G6(UG*5BAoXwlwdB~E?Pk;t;%?(nt~6(2h$E7GcGx18s{sa?@HN**EE>E%T+ z6n70NdS$bM9gC^=NZ0E&g>PJ0HghPkb*v(zKUbbqMee5Z zXVjqWdeYx@P0gDN2gxIb6DgefLz*jL8107QqQ^eQTghn~5B5~NbvIP}-}YO>W~Ja3 z-`kjNUAgf1P>9#zG5nVDrb674&-9NO1js6Ir&Wej3%Ej zGn%JX;nBdTl|Qq|Vzr9`_CKT7mmb*+G2$yqJAN>Yt6r;OnGb4{glg6Z38%ba3n|-* zmvJho*VcM+^XYHH7`!ajboWz0F8iI;Jdnzpv-d##bJB_tJ#8-e*aVN$yuM5^$}pq8 z6s%$z(@V*RSVoLHMJ8-lR!Rz)lf*s{`VW`PwR4y86uc^{Rlr{0`0k;Jw;U!|X%?*S ztthpceMIg~Q*(siBl3!TSxD$qK>e=D#VQ1V5!0se!_&z;>EYIInJueoVNspq6POv`IE+0Yc}{+3Z_F zM|W$kn{b|09#1BCw*5w}K{PUGN%AB9Y04x2Nq9-P^`wTQU)c?AefG885fGz;#AVw~(rK zcTet}UPE*k>-BX$t--`H(%z;@ww4lSXsULpzok8#tD~`1|0GUg7%P4c9%t^6u5ue@ zCyJu1-g2&E5o4ZJHbc4HH>+RwU(o@ztv!kOGLlWlan=}lM{8IWsXnQ3H0Rp{&u>M7 zn%=8s1XGwP@6Y)=JFB-(bvl+vhP`5f(oqV#j2gjPYG`@v< zvoyYs#9LZ1)$pB{R;3e#@|tR%arnF!B)jTMKsco=_cE}9s!KQm2GjQjJqJ%QAGwC` z^V!cW`UR7CW@BgA1$7oZL+rmvFZ5ZQ8cH+1oAZ`x%eLcEXya9*+;@zaoEJO=^IF0( zAb?#O1OhKOFI`uF4ZH-48~kM8?B;#UT*jWR11uBfpth5Bg*7pgW%JmT%(omyg+G6u|m+b&;#zd^m_G;oZcziB#sAK$0bhSANh)HpFYd>uNI z6~N!YxW@X%e_FAhy<1?Koy#!@($~D@3I&qDM$b(6T>H%_7g6TL(E?&xBM#WIyxn*gMA7Sttsc6LS_fnbC1H1%K_dt=&@x6 z&LLupgy$)w6tGLu*$ zDKD;;Js|D~xWPF<>~Ya@4`G)rhk1LD+k?|7g7)6#{`xJgrg9JJg(kJ2iFV%bz5W~h zvW`*8WBjFlow1wwQ&|%Gi|r`85TNHINxWV7+ybJ-@+_|y{nUSlGO53*iKw^f;mEA0 z0i8+w9$IkQ@Aa1Sn5MfWM;WPx?hHF7sLhYnvbL&b1*Ebc%Z6MkxKqUmmd|-hu~&WP z$=D`pqm;5?Xs6V_{?Go`;Cbq?p1G7N+Qkl=5_|f+7LW8H=E8{>#-)W8|EWvX6h4eOo3zHnPOOVW-=f7M)=WKA=F6iPpo9hLaflGZe zz!h}7;T@RAXp-y)Rm^rU2OME3$xpxwY+}6>{LI1A9`Rkcc#IF9#KZh9@J9iG(=h*E zu*m$9z>lBWQ_7P8Y6FG$3ivE81p>enzzLXWijf`y8^LGAZi|iuUcpg*31AWQ%t;HLfP@x-d`D77~nS*xQue0U@!tIdGm#UqTfO#T?#Z^FpHMU5Z9YYyPPgdx_QuqSve9v}f20NzHrRpO4cx!7u zg%jKOJ>@*-fj%lSh>NO0-#I)h`BtX@-U0D9i`PIbUew&l3>n*^sAN7GIRu-r7#nso zcCy~|sjFhyl&+M#pX`5If22@3%0@-RC$6_{zRw-*O{K=ki3dsDEGGd=i0N%RCi$`Fd1}BbCotd$3bAVbu>Y<2qe|HA*ADTssYUNOg|5h7{0iR=rQZ%i8e$_SP0(DP*D3U^_rKG~QnN|_SFokt# z^&WUO2Xicd#(AsEa-c%see)lv7Y$J?Lax(?p?gR@;~1j?{*QUK@;2Pex|>IWec4}< z$Dv2u`Qcw6Ezfqf42l7Q94nydV6<7E@DP7}W9J0h!A`aXrtyY^ov<}vNnZz@1Vj~Y zAtvxEcNnq-fn;;xTkw5&lkgb7Z1s9!6aR)INmwliGkYrZ6>M!VgJ^KCG!=@5)q*l; z0ZgU^3y;IAE2aoBcwx@p!f5Di5-zlbj)l7me}@dKodsW^GRL}!>IJCUN_1asY^3|MIfAK@!o&*xSJ9WSa{d=W z>b;l$8njluT?9a@rR068Ra;UF1H#cjJtb zCwwz~RaP8-j^;=Lg+E6%H*|=XwfgiC%br|@dzZiQ7zV8up zA@HA0Iq4mEzOA4n0o>DUo_Pu!He5-V32xMqLNmc}*B}30s|Df6fD;idOZT`6mgE$ zARtLI{}*KAeVPr*$t6b#AGx5M$1})`s%DdiBoAr|N_L93){STWEs{~@$Bz;KH6w(M z^XM&}G7M(Ew)=**aUxAlBZqk>)b_GxHL=(W=}6r=E=_`wSChKMOv=Oc7e#f{mP{+6 zl3o;VhSQivAx0R%y6IVmc5^!HmmueP#in-16fjcRB~73l8UL zvx+|wK`dTIExwMuVND~(=3EIri4O3lc&^cA1i_~Jp(BEO z@@b+G;ZJxuu>*2sO~Wrj0X3ueB*?BP5%Yko(-)!@kVBjS*#}t#J0h0QO3!651TC?T zhi*c@oBo1apq0{nL=SO6_y#{sa2emR@5Hp~6WD6Js1Qe4*r~KCWEbWaw*)al+k-B` zBIG}h7tm{@%sv^4fiIc)2%F(JNeg~Pv0;KoFh%|+V+j^4+g?2h#iU@tP2`d!CG`ek zDYl92f#t;eAPk;@@AWtg)nYRHb;6TqjajA81j!I9aC1F_{}Id6{-XP$9h%arP~?@W zxxgEVP+U(-hz6MG!hUz^Z%B6cr;04NzXQ)}glDyUU;7_KZ7htj628)P z1r`cxR98Ia3;vcH*?R~|#B#Hz{402(Xr5te$#bwuH@#d&?baAoEvV>JrPP$MvS4rBCTY? z2i!cdO?}G*Y5gxRSK~4%kn7o7a6X*Ir#dmxP>!TdO4%MBOEyZd}>Lgpy zuh}!iS*RxIH1QL4kIcc3B1iovVa-UE+jO)Nes7zJxWWyluFw}aPB_symC;y>WqYJU zbu*+{k~8a%NG6NVW-&#hqU%X%L^1IwauV)|Kl3|=Ex=y6zD6FRUu?_ZVRVUUDMUlU zg$v|-%_XM4Y?)eA8ztGUvRQ8;E>W~)J{K9wS0p~bZ%B0!{n(gfnje8`MD?y`k!t*z zZ8hwJF-+Ca6x2lEA^Xy_j8P(0G!)hFB`$iRm?pZV1v7JqW_4|19iFdDkBG$r;aUCtn)T^hwOHM;_#PZmWc7kL&O2*77kdic=5;qcsL!TOYv`eFt&G>#(x(-aYXcaOX^470vvJKi z#RJxt&~vh@ob=UqB-gn+oKr;i04r+~{2Qn*g8Vw~WGGkq0BB#mNBk3{JLidv`Ki`%_ylL(csnW-x^N5)rF<8P zr9OyHDR0(V@HgkZRWIhhN{v-c6Zpqnmi;SGhuTUP2yRah`vZlx&bdUTkYinkH41N< zOhT?h;p{#`7Mej$&?TZKW%o2u$o;&vs!*hD-62IVQXW?>3q-s_u1kvHZ>vj026&%y z3VsZhSc}k)aEVDI;sM98j_L_X8A+za#3N-vYE1Md_qb9da$J`@;lr(o`%T({Zw}Fl zPhqcC2Z*L)j?U4zKT5IgK&z2sCLr98xG}%z?x=F>E@^Hnol6^4cjcdQ0~L>Dqib); zK1pk0hoo~PwjuH2aM88Za|ndMoIS9;c!>1~@)nzKG6K6IUzx$WB!he1ADTp6U&%97 zqIO!&fMTsWVr`+UM#+thl}P0qgMW%nNRNA8B<6|#a{e7#L;SKHLr^@>PYK>3DpKIyt@xm9lG6wDxmamE zj;tagO+G^$%%6Tz>(q6m=7Kt_!+iaDWqm6xdykybv@4}esy951*(5%yn;9$=%~gH% zuE#Uv_nmH`Et03!Tj56{bJO1;A8eG?(_2*vSJ!lfR?I41)$Ufs$-LeYQFAn@p)rqS z7bVrRDUE?XnsMqck59^Pj7mpyc?9dO#i+!<&0M)%v=F#MJ>I>i`bO2>j=tJ+Ma`|7 zN#mKRO@C8_Np=l?Pz$5Hbj=1WPa(#ZU_-SC(4Bh8=SfAfDeM!^r$ zE%Y1Vy$QM6MQ~5V6%`d$1vtq+!z_>C2{n3=qf)#CPO&_Q_rNj6b?7=cj&!*7o~X6l zq1jPnnJ;KSh=#Ony7&0UgejV0JU>FIJd2(3|4*Kcd3rQT-k@!c4I%-$!16J^0FfIH zAqNmMl5wlG!l6vt#FU5ReK%Z{g{DR8Vx+e5gX#m4g%NQ|AJI?$bFx#!Ll00=iXV5> z6JN0%mOn8Cy36=35{}%g6SZL4&e9)^*EJb=wFYn1KdI-n{mR1l7`2`JT=-ALuq?n| zFN>1sJ+j4*MK2r)f={Ga&BctcvnI3Pn<%&TN%PC5*`+m&aSbuK|L8aAt5Pep#@g~V z?^J#2?C{+RFJ+8>iS&wmsfUl4DY@gwA(n~uSp9+S#rK*x!zlV+ZA^21duqwchVw0- zaz*-(CS~gHnvD(1*KAY;>eS(w{EB9(zrU2NjQ21So66*lB>a&0os~ICAz+g@I0UP$ z+A;j7Y-+LT;PMJimaMm~>UQ$4&UdvIF^z3x5)wjhdO`W(V{3qEH7?(^R*c7Xfy#sI zLbF@4Lhi{G55@kVbCv0kfAz1T{(e#I+bpN<&!o%AgB?0bM@(v~EfouiY8~)l)1J!sJ!V$( zzf4}p?+psp$JM`dK^&gzQq>3EY`X^e2%tBoN$C7JD|Zo00lVV;KmxO?P}*~eHJItz zS;y{7`m4>3BaGhGbb(tReBD6f)vbQ0^#aJQMyg35({8WqJRdY46K@dkj27big|v#8 zerF)9;79i^;Bm&zjy$j|iPdTjUWhJlyuyzQ?$ks4Q>#yCk^~8^W=cE3MZ3GQDZ*^? zSE8rF`$i@B45+Mpu+JU(vw+)m61tm_)Xs!nC7HHFKo6tM8>d1~g2}qw(EHV+YB}`F z)k;wV&$as@T?3m~Scu$VSEJ3?#HlVf?TsZS70mAZfNxCy(6$MWNxayMU?-v;HzZ&g z!I8T8=#&)6%Q?nsB!fRJ%l3>dTeugSKjtE3FhgOuvW zs@ba@6gTC=F5jeQqzU#Oq7&k|7F)2}_L!}wMERu zfzsObp*cO4q|<($?bqw$JtjBz(5|eSY+x~qt;;p>?1!ey6lT1D6~>ZvAh$4T%kmm= zX6Po`f1n`M03*hURYHXqzfTpPb- zEv~%P9@Y)!K-#J*tqI7nS)#P&8_ecO!vw7>Gl{)IO~IUz7rdJp;X{^yPf~M#954{G zqB|RO4SCca2Ojd9(Y%VE@8M{05KLZ0XgmdjHnECWVWL^8gf9Gh<%H!6V&q@hcwhJ| zW99}6;kBfqzO}+@(a*YSh5v*cYO92n`hIT8fTBIz_1O@xs!v@3ZMEUaA?SsfPCO2; zSosSNhvV{_h96<4)6Wn3W6?=2yVkbj3iixGnK>&yeU~w5H=K@hb#tc}67rUT^pZ zANKIk{=}!Qx};iy<=dQ;1)%%PzKi&%g%J-kLAvtZ4+SV)(nI^T@@I)Vx^K!hMHh9< zl%gR8Ee?_z-+c{X;%pBWZH_2#)omq9E5&~2qEiuP(xRa^=QY)0hCzI}!f>17Wa%~?srs>h0VqAfPxq)vpFc_^X6P8eN6 zlhNipkHH5GAJWGABJ?K`*LEqj+oKn@9a0|)v1t0Kyzi?vc*+0sFxBMArmlLSz$CM5 z=SWV9R+(oJ{&=O)dvrSb=|D&|TEyI(UmIOg-XW`dSstN0PcE3dkuG{3N!PIJv@y zE^VD$HL*97?Ww6}j)Ua78rB|?AGwl!u;5aC4d*~|4UNj(8S2j9@CMzNv)BM&HIIXW zSvv>UcLMYcM41mT-b||K1-?o1t1f~6W9Tf~qe$8?`r^8aOI!)@gt&V`4C3xS?%LDS zKJF8D5(p67-Q8tzcXwEPfp>mD*SG4btDgHlClgGhewX&rV;GLox0Rz=X;9eW6C4gC ziQUiZfwubJ7wm?vx!8yvL+`hpuD&9xSaGG!L|p55-snn{sK=7T_)Cd3fW1oDV8=0jF-e?NuTteDRL@ZH?!ji zRd~ZDpNh!-%%y|X5j`!f_H|P_AItk2hT7vr&ZK~rA@VWGKJ)0ZXnMS%A#*SDp!Prv z%86G`^GA966b&xtg`q_E#!=;h6~_kERp2sG%iP+cfxl#H>i_NCF3fDwbk&ikQykkz zmu{eKY+jmK&WtyP#ALEBY4`Z^cnzvJmk)xEveg?#mYv*SKhRToYt8KD$eIHyrx17Q ztCx)zv^M_g|4tH-ySi_bhSNgYH)c*^Ty7S}II{VM0DltC#&S2FEZ8MCulFbm*b%?v zdS(7*g;`mXxb6%QQul1tbiv3*)lg#7e`N0^6(u*R7rUt$rx=npQuHsjr>V@pihEC+ z#XVI%(Ew^LqYCpC&aE2NlDOKD}qz&Sgyz|sF%02#P`qi=rrd>@FVV2RUhiLBp#T!f8-}?{3ivsN3l~ zpoH$PIoOnd=kz@#m*P{5zo>cGIqVzV4%6`-FD4?js*S1^ zl%SEm10ql4s3K zU|*8`OuNfHEgKy*lfR!B<7*Ty#lAULisk6-;Uo3yn;-VLHH|O_=|QrOQHAWKI_n>E zKG7X?$qmU&C(Y8LBDTBgPa1<8pe&61haV={o_{NIDlPsNnWZHPnN=<)cJ8!=#*q1LjAwTQ9U37{l zSP@+}ahYf5KMhR-q1vjZ!M2&NoD;taN!~EoV(&$DR6%D?2q{)0Aajp=o4?feh9lHLSO}u8>~TdAi^d zQ`Y)#YB_t4c~WF3_oIG}Z>7Ll?c)4dlq?S&qSk!fV%0IXzHt2!_0h)m)if!Cw0gxf z)-1}2!GYTQv{!vL1wy8CCzfi=;~X@f1&|gCW&Ut-&%ISV3nyyA@r|Z ztG^~Z+^`IJB`PP?aK4B~QvcU5N4$~VU3edWSo2c5Bo&MfKxCPVJP z-z)vNXGmFHXLy?_pe~NzL2HBA0x84HUM(zTVfCX#VH|$pSaBe)cHwUzMvxg1DM=Uk zdG7~v#V;KpX`|%ua#yy9b+zLeXOQEqF>w!ZL1+r^BJT(b;olI9ubU`%D9kQM5RDae zEaZs8#rMKr0u+g@_prnO20Cs4cS}o_(U}^tcY6`*zWB7-lQSP^0%t57l5DJ}wP_H@Nmx|qY0msAtn8?t42iTpg`Wy)y5HNri7jwlbW@|KEsV``^# zi7mRWe?K+HWZmpSpJv!8XD~MD^Tj5XhxQq5kUgl8S6$%RtLNoz;jK~bPwo}ODRRT^ z2+zvydGo|c1kDKmHew(83=~GkGxHc)VA~@ZiGH``x@eeLZ$3!9$$D+vR^`F5sIqeX zc{8=<YGBa=z-lN;VH(!jtx|6 zR(9*pN)7vAb6n15uG+{?dd!d1b%${TPt?1;t3vTQ(I=(|nCH5!Do3&F+E?eKac8#7PwL=ZFdD;B1Pbj(?*h>RRk_n6@oCxJ z?q1S{^}~k66v^rv*n3*Qiq8Tz{q)kWGy8I}D#7 z=5dw@7~)^t-{gaUl3!BcE150Wm7O8kD6&dy1oOp$kn`Yo;Jha$?FA<}G($L5~R^ievaiBCYs6>9RN&SXE9H-hgWDP$rTPnJTjLxzV??vV$bO1w$ zYe-+xV8Ip%jK^$$vjAoz^Qy(K9=h#sFn#E{}o0MA4_kEW)Y0V zka!IKAi+cYAHF!m3wVuf_XH)UFgwRYuph-*uW?fJP3mb}iLMX|<+W%-IgxysW^ALI zfTy}!>L;vIZeR3ORHu-~pAwVhS;1d{MwykTNRop;cjSY9*v^(=HnsJd@;}bX7I$e3 z_q3V9uI8OKb~aG>C-vt`ngz#nBNs`8|7bko*`mj)wBV!SHwua;8+a;fckGv3z|S$k^>iP^8a6LO#v6^`SZE^K`_+iagO2!DHO*M-q2y^te;%!9~ZECPd45_wy zHUUb7pW`73LhLbbV}&hiP|Rlo0|OExXL;XY<`eGb?)~-dy!{;;inaXHt<9Okg2(3a zxXYr^hFQV&Vn@w6&k7)1+39#s5+hTX?lAYQhUIkjxRuj^_nh!$24)F2W68U^jlA;S z@M14Mqmz|cCeXHO;yQ&JOfA9jqRYCao(1CXs?&~NBopPnEuHwfsyO9$JgPQT>WBT) z;Lmo(xJ}O+#$e8r_L5QPHF|nx2Rguf6z75pIl`ckXd!>5dlZ^1TxoX+jR6|Xzfq4S zX9a=IB$Fg}kuTIX<~!sJy}#}ovYNTJ*a^|I4`;kb#N6|-dl4G{df;88LHNWn5u=MA z+WR7a4d;}cKss>bW3j03e5}e3gTsIz0wXv85MA~aARy8%on=_ z?t^tew|gVJR`SbU3ZIj54DY0cf^uT7)F4b1lcak@9`qa1>!RdiaIIXlOZNr}F_M#5v4QGKy#y&XsV8 zZ0c3Xe*9Cl#9~qCD~JON@yxVi;CAdyOsUinQw63bS?C`I!~EYQ3_ zCs|k`GX!%1z1pAh4S1)DsM;f0q|C@4kPIuz)6&7Y@|u{jpiWjBXb=7%NbYiJAue*@ zN|$3M^+C~)`4cimoM=+<{l#aE(lOz@fb-+Pzs`8LK2rA@8hr{3-0#M!(vX=BgX(F4xcUI;+MNk z$0?$5w!5&mz;)$o#lMY*pYi9U3Qs{RpiJ5nccZ1L<@WG!g0hX?&Zks zxP<@O|2XzTG}$c#Qv&ns=3)+Dv7%N!fiW8VBx5m;aSDlhY+4hWDCRsbm*UrX^qkdr z3IAt`274itMRG7iJk@^(W&`xQ6`=b;S34?7mhO_zA?o>FlH>SoK@K|uuNGD{s zRy^h?jcZtjo|AT#enq9yU)fC57ivmgfqaJ!N4!PO!#4hAWQnCZOdurqs@*rl7m3E# zqbsnNmiuun_Kg{bI$^Ksk0Fn+9i`#OGEA3s32DUYll_r+%r9a9nTLMyXCoug!)|8y z0@`jL26tIo?s)hm)C43!n528mMw^<$BlitQzB@KgEstVQs4d1TTlctFODsDw?# zO8=f|nZn>CG^rA757hS8KPIjDTY`jf*eC_Uai) zbl75@h_HhvD;D_&LO*36+?GPOh*kDI&?a1j7-5ID9fE!E|61c|=b?wq1GVd+qoxzZ zLTH2G>EZ^cTlXtzCe*6=6@CjctN!($3@H_#-3B4Pe2V>HsF+BH*FdITSN;!3)a^%e zhpIX!*E&JTZL5k$LSZeTi^oGgrtOI*Auq%1@Bzq6OZIyS%~zdtTLL*LQtcl?Q;44y zPJs1NEBaryqfjWTiszr%cNI30v7F+UT(EX)Jdfdf+sZ8a|D=~?S|r01P^A#07zS(gG*`WA1E=Pa_5jJX2+%ZAZY@ey&!BfF3@LB0 z62e9+)^ncwoRw?&{jTF=ps3bn8v#p_pwIemjT*j}ZVBlXHCS6r^{f7>ai=dT+^zb| z?j{zrE@d>C##hX$Il@;An(w4{1&vv z=L~KG2e~fCir^!5BhXHy6v$QHLjzeK6hG0@CZ56;WmX)QC!v)5DKZ*LPsfNBlpD7T zzlG95C*a;_t

uh*rDqLwBHLyJX}S3X1a;Hxwvqwql&Zxp9X)LcYFYzKkPx%~KJ> zvhMWR_JSeZ!k zs@z2ERNu;*jQ>)dNZW)bDmTSBU`rK-kQLY@ImxFTCCe7M-bZ#2X?E+8ad?@qQ(oSD zo?+=jnoSKw#1)f&*&kxAVM%TaPSTG~%g1(WYh$lq&g$hMEL5+2|o~&`eoU;4ww1^#G1$&wy=Z$%3j5&#FUox3+n;%j>SUxHbq%>&>1d zuk3fmaLVh21Nuz*&ghp~m_-W;vAAO#JU^-a=09_GQ_w{Ftk21wN{0DaZH|of|l5IvaeR_6@{s?o?LQm}{c1v`hCZ2mP$WGNOnCvw{`9D#SbFG{QEVl`g zjgXGyg|(E>E>RbnI~dKi5ysoB)Z#w|SN7Yi!#WywY075JHhz6{p4v|65Hw2JEV}1q zr}!IK;oKqX0GT#AA{>h0#+wgugD9n@nLKh$m4VLhD2~^i6YS4&(fSGhO$kvO#evbI zRnvjSz-Nl3l1?vQc^G)b`518xnqzYlSHb0+U{i%yL1r5c13}dtdVk=4QM-1zM6tL{ zV*^GcA5$5@Pf@#-9@4(Ro5*;y*^1(+!U{Upzf>OrID&`6;4pkQB7KurQE3uNjjz|xA2F@${pq6z&hfs z%;e>PZzi_6Bw-Bvz3nYD3{PN=)3urYZtT_u8GckcsW0j;6lhg+og(v<(pnpkB$l63 zUyD2?>r_<-h7(l9L$6tQsGR4LjsB52+l@soTd0wh+UIRM8%JomS_3M&)d|h!0!QT? z(}m1dMXTXwQi!}rZ(|`IxNBzw&cJ`EM|fF`#mdnx#ps~i%gzJA2@QR(HlZiE0o2Uw za;xxDoo)Xw|D>|1^(DH@4IDVs9!mtbbY;MiwxJngisLIyj_l)M z$Mv^()BQ8FO9i#=F>1PKhhwKQUNY9YULGsWqTKHWY3f>i=Tt^bsdw8p*8H5cEqwO% zv>3BDw*0-^^ozMaBJKu{Z8?yuv+aBAj>~e-6rXCmnik%d#AtTMkv;1DzO7r zkYhWVMR?83wngIF;=3(YKv1^H^c(n`T4cB-*&kP;+Yai(0yP_@MgIP(9gvf|S#c10 z?-VDy2p_VkB*q{{5~IBaT2M`C4TP2y7c|d+{$x9t#=&U||I_~qcgD@p-i6^>3ew3!%-58K*ci*V1zo|c_@ zbL9#18r^}ym&TRai;KVMH)wV(oUT2l?u#X>@2cR?F4Y)ivfn3#zha#GR$001y3+~V zNF20HMW5go8Yi`kZoXBy&^*@MRwy-2GzBjn(9bZeOF63z(p$#{tIM=VC|{{mAMm>) z-=X~KzJYk9NOrn``^dJ~(oiE&)&MnEc6_h6W)il$6uKHztuc#Bb>`-UDQ%j)rnuP2 zs`m!J&|;;l_Kn|Zd69aV`x?Ti1e~5?cjZ!B6`Dz$sr|mZwYsNl#?rVtUY^&INsSR1 z;k~0t4-@H~uc$+j58BQ%fFSqgf7t2XEyjmD`+0Po<*nU*x%#(wqgAXj8?3BlEbD0` zO8W;INH=p&^yN^U)9-aN=mm*x7WQp#934DMD=Zz^e~jUm8`pE0`8yrx{KMXzaI1YbR~?DAxbd5U#+Vihv%Rgfhh02I*DZEywB>>(y0!B(gPw{m&=a9>#MQ;XTt8{XT9S@ojHrU!o;ey zP3;)akPz3hTap|($MhET3A|y5kk0l#p*2Gz-E34Jpf?WN6xHwr>ou~;$cn1_efz-= z#pLd{QX>0Jr!O=%t*%WDF%!l#pM&m2d@xOg^8?T5o8S}Pmo>+cD7RRZAF|2eoqRd! zYWpD#Ur{tVaeHD9Vu9Tnq%u~EIt08<`GzG#6@E^mJxVBw*gD{zNeXs zrMgurl~|^ugM2)eZxewZ!D=gC_v}$b7WH&Zm4C=iYG=q>Qg620mfOVlnPX)uBi0%E zWZ{7ubmNG9-VfCRBGHYfc!ytfOtJj=$u?{}5ua6wbidMu6$N(2XWkDKiw?OczKJXlxUMJv^k0) zgkOcB3u!%9$ZdbuvLGwFwc4yswKZQhUX7n@tTfn0EY!WyRRr=iG!5JPvhuA8c59Z) z6l)w!M564s%_A(F=qQ=BuCXR7XWy!Abyw4o6{8zziPcMU$aA8gei8L?@T=|?#&N%l z_OtAb?sr9GT5qRw%rhCK(tb+Lh4P+bP}_HKyxtU|aCPHj!kv zpTzvxVs4@uQ0YCF?OJE(g?+5@A$-?*oO~p*uc&Z&4LCD9cBwzeO-t*44L(Z{_L!uC z$hV!dp}&Inv>KpQep>S^IMc(VUje^$d8^4r)b_a29-V7lBuhuTimZm>k!RVH2HlaH zY4&~Jk^2dS-K)`&kvBVH(FMUfT8^PjeqE+KwA*8&?iG5@)n46;jj-RN5Mn;oUBo)9 zwD8ojSeYT~%s`;bHTAXS#rb4{f7clTi#*uQB+`P{wTva^_zf6)@LL`yby0Y)Ym(|a zUT6PC{s{N7zC?K7BMPZYtJJfy;3b)=n$(TGZpuvwqdQ+J$3_me^(q>JyPH$xr~Ud3 zFXh%AH!R`82G=@OC9%_Cro4)nWitxjg&PYe4r=s7R!F~8$4mu#%C!~ow>#%)vLofK z&()d1YV(jP-LJ=xt(@=iL_1Rc+f|~xC41@+B>OkF?vgHpP7Rx$ixJ?>9n`qjy zvJoi5hWbe9mH)J@pjg$;R-B+dZ%7tBqn{%MQ=FIwsB_Aq*?Z}4Gi$gu#kxrgMf6C&Pmb08W zri?Y5Cf;zgFK@kIyYB_TM^TrPNt_0>Z zhQpDE*kRZK-yGgV^t1BDL0B^!oK0+xoRl zdsI6LuTyp?^U@~K*UFzqDp|{90^dxoi5Tfj6Mz`F;&S2cPX8|7Qs=gx+O`UQ%VqfQ z>S6OCwy5rcadrKO#((wN!aT}2Ej@JueTF(MvV`TVwDV2lddNOG_X<4m9m`J?>>jA= z^ezeSYu79-@9Vw*`BvTQoXXl(H@z*nuC_6$nNe_yOg1*B*3dM%wUI&0_3FL8shsnQ zQ_jcvPl+=_H}ZcCLmgSg=<>hRSIgcG_DNY)q5T1@X|=rWO|`cg)^|7*2uP1xh*U?K zmGM^OY-YC3&37S(P|=-V@Yl+=4~@%bZsfJM6}?}ZtX@!7F>DWBt6VcQhN-Rju;g?t zqanCQQs7Dwb~r42P2JPHHR3mOlp(=4g_ElO$7M7hm5WxLp)D*6>3Kv~RBH6^8JB9V zAum}04bwRH*sDo@*T3NgP<|Hf=AEP8OqB@$)}{!p$d8M8pBBFmI68((c8i{^Tt>QH zAJ+AU0yeJEEu?vq9bg*$9aYC}W3HoLs=LQ#v!)e1;-+#^7Cz?t@`Mrdg|h_x-ov6V zqAtgJ;5o2qIo3FeKC5$P(=Wz$O&9qys}vefea-&OqR}64^|ddVxA?XNj_hZGWefAU ze?%VPX9csw%e>Vh8{oU67Vriumo2Gl(?_T<9@Oh0Mn$voVL@=0Kz5`v7KH^zCpxQH_>wN6M6F(r|{I|XY6F`VAv1t9xTASO5lKYIC+R5 zXPneCO&W3v9PU;ELME&Ec*_3U%iMbB+ zN{vVI0@fH+Tv!)pr6Sk6f*&a(Ia%1(xVTqUPU`#1_@gSKdkxW0^RqKfc&L6^+fnND zrh=BFN}Mv;v@2&l?TEo8=?N3k!eM+)u;;cXTCLeu;alW{Y?$?^6A3nwEd7=}r|Ocul#UJ3(Yi z-=CxtSF)CbRsk!xQcsQKUqOKVPidZbnq_u0p|0CJgXPm`Cre@1k=jHloGsK5)J5F? z=!KPBehPDSPK;nR`)krjks~iQbe6bXfOsO3aiR`~C=dhA^cC0M_;Zpt7b=w8Lp(5B#V--KCCUZAg=a%9ikKs{tmDsw zHiy&-enUSzX`(Z*kHbTt4X$cG*~lVBYtE6__zw6mg@H4 z7w_UM#&Qx;ctz-s5FbGi+ULm>MWJDiZopjRe4DW0Z_RpjZg(7tQN1q=C3ob85$i- zd!_qHi;TYP8x+32v*9MKOE;n98{>dRu;@7Jjp}NAFV|M-6?~PSD2F|{!aU-Zqe+~D zN3@jJ{nd3=+1HTKu>-u@ByVeDPbcqbscQ(JzBc=nuo-rSPm8KpMY^T&g`9RxQt(FJ z1?59euHav}!f}^)jK%vvt1TJyRUEJH?^l8jjgNXRvM6NNt`YUisO9Z$CI6#0x5O@* zz`S9Kj<@E_*SiOsc}&ez%L>btBOGsuj>)E)H8n?uD-?g%PhUP$LNrzm?q)tDt?bXJ z51>5lK3+UTckT$xJj~>@pm8tRr;WRV#XKkN7t0DwsszU`qE^|#Ho8b%dS1;C-K~s* zD#R%@8jgqfWc})fCqQ}Arji}NSIUk>u96k>WpOgFfu#zHk$Q32?xoUSf_S_0&@FLz z%PzjKZkFPSca=3(?28 zJL4pJS@?*Rf*zz9*t^l;8V9Z$+FWpsXGEd&K*3nFEM}$=f3-lC?C=DLA`YmE zNE?PkP#;A(DC0h)mG%>48-`O4y)uG%uQM(WV0W~QN^RgSHV;Kt@W&WJ0*+XyCg&_G zFqM$w|A6r_iq;jLT6z_o4ZWxg;eUXx)Buzr=yrW`RWkIcX&~=6JdLt3H4ToVFN@N^ z0L!?*58lTm++vaG0;cVDgeiv902p7}1$Tq|hA>_qxUuO7nE`&Mgj9w|Q)w&m?4{jI zyM@Ejk8F8V5R}b*v%m)0DkyNPf!#!_?dHHe5-a6J@nK33WD7*m?73OMcE$)&nIwpL ztzxlcGka}r4H(E(FLVaC@v9?WNTY$ZOP!e1Pv|Cq?fi%W~Yso57rvSU?WGkF1eYNtG5ChCk$J zIVj&lo9%u|SHNk+4PFWS1c3O-$VAp`fj{EW&?2-#VoSdYzaqui645^hJ9&?Ir=|Qo z*^(qd{dY^0h{A0*Sd19#W2JUTH{QpcD!(eO=gpB>|2^9p6F~wsK0% z-{=HaY~t0`@s=2_mN@Zu>wU5;RuwIn)FphV-W-uE`l!6;Unc&onC^B_GD22le@XHV z-+`QBsk>(hZn8IaI?x_-Znw{m@ZOig#OXu zZq*pKOTaq$D*MlpRfGhY#JoSyCx~V{_K&93aVmPsYT4Z8&i>*G-to4Ri^=>i&5x3t zgtLt|!(WR0bqD?3#2M->ZkGX?;<&?930G!=v1P5LuSI^uKb2(qbt0fT3|Wk@l#m#RV{=}T%VkWeO3MTI~7a> zakwU<6ubrx##5xx@a0gQbR{yx4mb@=CAP71fht1NbQ;)C zsY@D4EvQQnw2gTzw z0W7AtNsTp465miAs!#$A%9nYsfXB+e(gl)gg?HQm$#Z#Bs58iwrTDxCKN4m0rb;Vt z+-{e2H?~)FT@+zLm_s59zPYhlY;A}s|0!Otdyppv;}@!#dRcR?+*$dp{(UZAIfXPP^_9Yl>KyYa4mm@+oweQl<=GTIGx-9xJW4F)I>h+l zC;5&+%kf1*XD=moRqW+_4r`Ej+vu_XNrQM7Wqr&x>TBXH+rRcHQNr0-vJ!vB^USWs zx%|O}3HS)1Q}j)2P-G4wunb_Nml^#m5j%fF`@nZLH_$AoiF?-K*s-LE@M2+f?Re~= zD606iB}3w#br=f+tWu7n4<&y`E6@SS$DlHl3|{hTLA|BBUF?x((6Fs5vI6ele8eaq zMzO_4Nhj1qpzEbXaSSSuzRwCmL!k1MLgX`aDmn}~4bKnqM}}d{ONT)4W0x2t3CXsl zBa@H=>>sEXww;`Y+{U(6E0Fh^z7(Nd`Yr=2bm`cPdU5Im0Uy{);nVo!pQ-2y#v;$`|%}{GgUsD*wY%Z!i2Njs! z7A8T9j3tYDp#uH7WIL!@`z$x%MgWS!*!6Pl%Q_sWOtl_y+`kf+>a zmk#+5OPF(;t;-CJ3R7L>@rs9r!!;iY=I9d|MrWpIkC0pvyEU2AMG>&-Bcpf$MLEo_ zbw8(Qvvw_GN8Mfx zkCj)(QZHh^$o)?h%N3^+N?-o8geZlBU}N|ixtl0)fxqS3%N{mFwuJ6fj?<;%Y^Gp+ zP%m}7`Va3@?K72SkCkpxUKhINmMfl#LeodcKZ>*BugQJ`)Ns1YSt49uP2_;EhZl}W zjZSK8J+#uM9DNJ#p~R?Ez?WJdrA}fj{adjF^vt;|KL#F2Yqj*~E8^v{ankSMVMG#y zFBpYu;7J~V*crInX)`tk39(s@W+E5KbY(Vjss>RsAumc$xgH&tQ!P7*&Pxj=exb4P zafBaQ96k}p&>H`D*gcf&5s3w$5~r&u7wxrqf*eGTlGZCyY)EH8l!ad@I%(i zTbyShjxu*!D%^p;ZhS?2Z68^cOJuiJ7OlsZH?Pn7hJ7`COm)H-M(a2Z`c5AlCPc|v zYyURnuIiJA2a>ON?(76#l8?7l!bJq7VO`gxvY!>F+gDWu72Ig$)Rr$g+01L8Bp)+T zNvi12hI;C@;EOtlao*>MW(E6&o3H90{@)Ih6~9D&R*U6@l9c)%?Y*`0D!f{0^_L1# zn@LUlMTw?C%DChr!$I0`^itg|W^wRJ?R3une2%G$cx&8>l>NeThdlX9@o%fmvKr7> zKdrTm6i}Ym+(xO)*P6D`u*}WI|1r9gp6a7m`=hh9e9pUIP<@i`;B!pnAvI4N_IhFcfoXQA`8P3GC$U!}KcnouLE3_2&So&klbYxgjA+dHq!AMt=_a)gVVfACyp2DPa!@Go%|SQhi8$3~yKFS> z==RMrk-YDSU@~mI4GFEs)K&lK^s3FphqV(`IXO!866MwOcvYz~EaA7(Mq!NnEWa&( z7_?uuR6fgREkTvVyZymK2+*+`n~a~fX+wVDAypFXcKzMrJWYyzN{*N6xy~)^Kjm_* zO~Pq~Q1idYi}Fm>?Vy!18>QA~DgIpHKYui~PxjJrBf5e(XtNs`#*b9~)O>8dUG!ev zXGXIhsPar!X-AYZ3Din^6htL9<&gKTv zNQ9S*2cpVr3l{XnR2ddM>t@$(NNMhP(XcnRx-FUXFpS>ZO`E+S-87S#=$WDK=5Sng zX`T6fb}H3M(Z;#M3Z7(X+3&t{wNd$pdPdeiTZDBsHEB{p+NV?I#m;SMrdyiH)#u5ZtYu$p~LJt?it*bCN=TtL}EzOu| z5@Oqv3JgYUTl8*S7q%^wtm(tHEf}F1#Fl#=QLMsDuJ>ezFx+7Q{uu+UPh;hnwAi_2 zuc9`GVQx``q#rVBNq9Y|FyDIQR8`5 z&X+HqH-=bc@!Bx)iA0@^BifA*6lI#f7|&%NF^x2=PmeR$=#`1PbSb)m=u8c&og3<` z+N-|e|5$O~;%T}dAE~%BZ#t16CppNlEi!wX7_@-+%fjw{P%$zyWk_B1Ah}~Oq4rwr zwEk%gufvXXuOT}G9PNmrHF$4tdBt4scEhxa^UbNmVlVQwwbCSu7tEcaluCZ(9#}4} zRc4M^no<8gxp9ewsS)$2*Nb8wwzBgj?Owpawsz(Q?=#Ic9K-yv#z;QPsaxkN3bSRZ z?SYeX1qx6)KR0PvRnxZ&!vLB5I(bwdn>He5RkxUt7^dpTVVMKAx7u=Ed*3wu;g`(! z(ti@3bh@hf0XW(2QdxtEb1%xfpx~U-gBU$J!@GYY<8spWo+~V647>9Ur!K6n?LIFi zV5s?|z`^^T@rY>p{50KpOAB~{`lIBT?SIN(X^)kI>@w`0lQA%oQz7iq6+yh`)FSo=$lUM-UhZM`F8##oe?vwtxmcwEU38y$_iL*JPdvEanwD9&GQdy zW+L9s8d8|IGf)agzQWA?dnCYB#r7AjeLv#*h)sd!(7dqP;$U3 z!v}PwkDtyFy*B@Wx)}ZKd`qztbF=$bR)A$#hvSJDD|>EFkZfMs)6O>8sKhgEzll51 zXImJ=hOo(|(}Xgh-{45p`UGn&tg`uERfG5$=iiFiIBIW4oWmon$=DTqbk>P(Kb0nJ zsAEVqA+fv7Mu|tSZf;e43;k>wrC#!jF@$iq@#f2NX57iq=6}q0ry_IpWhIFy!1RJJ&Z?LK}(|t%) z1uCYLYF=n&-(~CxzrjmoU-zyM?Bs2CoGp$P?pm?EGP>HPb6?H>>PBm>*DY_j0gZ2T zA?a9Qq<*S>t(@viUzNXszKbCOL+Sip1SR`qC3*pY~f7 zpQ(xJi0W_jG;mGbE@mimW+Tl0tEP>d&bgoWhUUT>O37u85!8o|Vm}qxdXMF86K``Y z5^5xsOO-`W*l*h0%JR5T$}^R1JXj*C>EW+qOs!ukxKMqdX}$1WUMyv=cy#h<`gvel z*ecdtiIvww?oDvABS&ymI%42h0TpO%X)T!}aZu38+a!ZPX4NHdE`3Jb6i{CEu`wI` znA=O%NDGsTX-B17!pO{D(BED+EpwSN$2EckXlTiTymEX@^Sq)|?4aDW^f!iy{VE%< zbec=eDQr^Jf`$d?iQLH~1BxVlqrO6e!eW^z$YZY;93x_O{Kdb3g!diH>ChyY?iXxS zr^v>am{mEVunGrd9W}3drGj0lt`C$0Ir^qeGAwC1)tgX;*)Ue%I18CbVdBA(l<$B&>`^cN(H(8LotNG@1rIH%YP%xnDsyPjlD}16Ge=f{wMN7J>4w%R2+OA=iG|Ip6_jceJ3x7wE9P!f53YMADAJ@AxrlyhfmE7wBd|K^x1tHWHffORGxXA3 zt<@s~W`3(%uKi$JNjnYfz{}|eK(68wQw;v%pqvoMR_De04!aZ@g%{xEDc>aPk;ze~ z<$7fOq#>##)Z2Z#<{jEI!&NJDCN+f;edVv3hvBc;^I0G8o3#?|6Z}YF zU%?%GWy)glb=(}aLiRtLJIP788c*{Wq|x9mJ%QDIO{Xj&b(kp_^{1UNQu4v{{~3<3 z?lNx~2Gm^Tyw#@{vfL`Hk$YDN>9=`Si5x9iQ$wbR;i zkl2QKEk9+kP5W$Jtcd1k*2y(Robl$v1;PAuQ(20eNM(2!rIIezODDxC_UJ}}ILGrlKedbW6GlSh-T@A6ebk512 z%Nx>mDfxnsY5Nt$mh889PpVf8HG0I}^(uFMnfLS+!!xX=r3!%)%KG7EL#= zsCgUx+N>|t!K~1pwtOzPtIIKYtKe_jj3~N9U>iS)D?eje=o1!ndI{jjDWUV~0w{zmYY`jf*K+bDU$H~atj zal}W(39=%9q0S`EyH{#g6Ztl`2LG0`#OS7}whOB7&92r%{4=Z*mKlvLT&Y=Bt`NAG za&ktCE*KpXj!4b=rx6boak{MmO4Svr&AnAyL@KR5^%J@W$f z9^BZEv!s<*K9t{NTbO-P6lgh}FiY~)^djQA{Gf4IfL_P4H}vdz3B?FX}M!nv)f2^tAv-5zmXUSkdl=u&wZ*112? z9Mct>;ab}gcl1=lwuLn1fTn%(j69F#8M8ZR5p3a%wlX0vbvl(@Ck*UpN~o7OwT4EV zk`J=>4_KglXzcaqubHmz-y%~EuGACHm5*!d)NIub+E9M48l$Tln=~}$g|d&@Ty9Wy z5KtkI#Irz^cun{qXo2jdpWOhe8tt+c9j>jgE|qrDmfO>>_{cq|ItBaj@2D@yYK7M|r`cxl0`1DWwNm>ORQ$hV%SCU%(cxoNSxycK6RBy$How){GJ@wO0fPx|Dudb2>aTsy?yxY# zX6{#W3)5TRYg$yZPLyTbQPe2m7|x_$m09&SW0{Igx~E|@)kW%y-+s+~YP{PD`^kF} zS;qL(IY>K(HNE|e=n{8Wo3h!;&uRHwGewBoltul-+bwU?yQHtowAciNkMUSosjAWt z=66m5>sGj31u!a_urNl?ysV)!56u`Zis58-`!tW_b##rXJ}0=<{mk$$%|L55*&KY>KuVEj3~d;y@o$Q(Pbzi>Z$*=#Li1^4HSi`B58 zwA;H76s_(QrB9LkYFiN7PhMy{7M84>Z~ox-Ox-g-noK};1*#|T3<*eshHLX$H~Wq_#-2g*L|9h z=Nf~{DD)thfmbQZSZCGaR7!4;;Jg~-tLT)bM!29VNE9M=`V9E4MMB0yUf?L7WpF$+%VjH)i+Ce@C0ms_igU6A70A0OU!|Vecv106 zGppi|%3nJ>?~a-Q%um^^*#OR(x&i11O%E9k7Q*d5#~}k^aqS1MMJX5+eT2N^8zd{B z1KiuPcsP&tUA_!HQ1OqlFA|YgsH#I&q*SO6qZ6iTw85w@qz}-6e)f3^KE*h$g|HWP z8>$dC6VGJp#0f+%=da`|ai5kXqliQ03i)qhN$zE(f-ofiRed1XQ?oP*A~X1__6-r_ z+Xt*899`R>b;Jm;Ofb>ZDgB>lglQ-zNqpZZYgi!F8IF{_mpdB9|NlnxLF049iq(2{ z@(xvkZu3+(^(E?e@N0Xvq{w#y@Pu6NdK}6j9RY|xvCUCBLO7w-i#<#9x#iymN6AX- z)iQ&u!ZI`0P5#G@qeGQTOv9posxu8cf*)y~>iNF@0ICae{RDoY251lQqPp88TtVja zG37>pJIjd#A+NH@siajl#qfe+5R%!4h zyD{gBZ?g8eq0?=uz56c&%khhq=cG1#UhO9K5nM`Zs(*(U(hrvXi>I+%b4u_GE}j&H zHwn&0`QSFm#2^!XR4( zpLaWZCb~qpK5-}dQUXN2z>?*eK{jlj%GqlP-e2>?IRY1f+cgsKKUS3JCnVw=VB*kY zzJKi}I8)eA!hqL_XJq|A9HciB3z2q(W8^M$l&UeP9i6UO>1FSJ1CO1}*aFx??Wmn4 zbPzrPQpK*#bAU&Z-?bs2Mz*4OA2dQymIXpfR09*f!+Gk>ksa_4ZBtMevJxEQ^#v_} z4me*$Um$$tJN0z=Yr#DYLrG`s&`wfqt`TZqsxKA00?RZXGXH>dU_!!jC_@JU#c#FHT2u+Balb+7R_>KSCK)B!DX3dpbCDQ zPz5F-`p8r$7TzlV4i*it zJDV(-zFILpCt)lg##cppf(m?B&`~9q9n83-S)$8|zpdR#os8@cT%dA;dcgbS6}!LbAt`Wq4xb@M$)cs- z?G~;Md5-mL;{%1k+P`Xoaf__GN_}uu~~kk-L-O?;%)1^g7>O`mI)a(YNpi^Z`8~*Kbvwz zd&(3W)Cznt%=BIcx$6hJ#=(KqBFQz0U}h6XDYe_~Xn*BC)2~%>6jhy<3brX1wx7rd zR=sK67SGd!*z{A5Xt|c$ASR~WRaNKNpB$h~ zbm9Ow2$zFHKH;M7bGxWU>%~8dOkK!l2LXDTPhot=Y*}5UrAkxZC5Uf z`G8$iOT+82FWSVwaNGqPIAOORK}&1H_Lv+8qh z2b!gcOHDwPT3yU$bSrQ?JRSWB4Gm1fQs7e0b__=rI9FhM&?f>dbWZc3IR*~aey)89 z>w&waBzzfMoa>0VK8xevX9Wjim%Jn%7m3s`{; zWvIYQC|+9tWup&E`#@VT@0?BWc#M_O1FNxBF*){8&>QZ8aPXkOD&#h<@_d4(;3u7* zpmPaV-d13YZUFrlu#);!vk#1Nm>k3#s_kdKLJ!ieGfQgXfNayVk|5xMDI{kk*kqKXxPzYz>!$sN z+V!`?e!#){fq@M8sBW5<3sOpHU2>4~k_zSAB+LrqJ=UO}6TiGj^IhN>hKwnN@t8>HC#zB^ZrXQ8R&2`N`3U8Q{?7Nw}jcvRWiT^Wf5bllc)IXK1 z36bh2$rt(^(E+OYcHR1I?F`4wR4Jrq^_rG3<7s1!f7xd%LyX0|?7|R3mta|Dxc-)S zWFkxNYkzoE=_vW95GM6TCH8wrF*L8-50Xzoy3-I+3!h-#G)VdKhH(8Gp{YWqXN%1R zR^3&pFk`JQQ(l?am)fn&igu$?)KfyL$xB*qzn>%zoap|M91ah4DkXLyFPrV|F6r!g zkK}q1 zMKe8W@lGtk={3&6gBWt69!{w9Bx2z!WjhFa3MtN@y25{rY1f*voQ|w$KDGXZOp43gpDHxE#vegbP39;OT4Pj%M59tenLi6P>JvTgql`j49@T(6Hy;QIM~9d^`~_%` zq0{3%TCR6;zJaQ!o3xM9cT|0^n$`KPZdP$*$J9oT?A>kc3?e0~`+@RPZ-f^ zxhK36G{r2FmiQEyMkvm@UNUUcBsi?so51O`q>eWYe=5JVGwGg1OIv%Hle4F`P@Hke z_if|(Lt+kER*Hs3h|DF@VL|bx!HO|Hpy7nt$<0yU1B`I|S7(JDHqhEg=77qu)(G~> z!XLI?UR2g*>k7fbWUYlRc8IZ`K1=6De!$({d{ef&rv&L zq{OY2+6m`4t|MQfzIFSouw+yD1&c(cD7b3oDV#D7nIy_NN&Adawb!(-28CvQ#J_qR zhzZKq%>YmMys&?^#O);6i(GX4NlwA0)lD#;Q6-dTn4YU`1uWA5&B08b(MNkB2{j}G zkEdPG*MmPJ`spZmbWkm|3Gw$GMfRd`?)}M7EXygIu;Qt;B2zOsx9q5~2htP_Fr0y> zX1W>r!IzV=_0fo8+6EmT9UuOUT7hm4;*j=ri*GnN75nZUMYQ8HoGkb+BBQ3So@|Dv&+?8BW{Y@We8C*)~DD$EGKGbb$to_)T9>wjA&A4b4XlC z4z~Q7Rzl1$-w3}!IGAif1U}Okd5IA8TaB?o%7k1k-yr-yt^T))^ZUez|oc^?e+4pWGJ~VxT#cg(!tHJ2>f2L zbmmXmgrea+C+V@-yy=^mxhdN_4{+k+{MvW%6C-c6ZV(lP$Zb2N+<+*{IfcQi-t*`L7;J^SMIl^x14M>sjY^-k6l?_D6!Y*r#oM#LkghTU?}DL-^KA zMO{F?8CDPRYB3(wE^!@Y@P)=WT+;1FD#~l7OLVbD{V`L0$VR>E^7V zZRv7t@|u=)$_cSt+rO&ak$j6(!wji2T?QruXpG6=Nw5F(M_`ewMaM@XS7ZLT{0ot3TQ-fsx<++6zgxk2+xZwH8BxUh}iHBO$^wg zr?8RU*}4$yvRi+u4{mk*PW&XoOOsnmkgxd}w*Sy&nXy&_7M?uLG8t=)9cH?Yy^YK= zl6Yx|%HWA_3^=PhMhx{9Q+k5tHieWBtDV9Llz3P2p(T?%koV1co$|@}Vo_0bNl(pw zR7K2d<1?xtGS0A?$_N4V^;BTML)|!Pkas)TOFnS3C%(vSPEEvka(aoy7H3rDt+3oT zDl_(&ZN}`ReWnb9*3~*K#X+j2;NK-#)mO9rT}Z1>NSQDXZ(J4UIeUGxLsa#QBkaYYH>Y3cCrqmC z_%3?jJG?DehI#yB)2aqIrVPPvRLuWqr^Htdl@ta{GtjjN$Ke+i=CUeew zc4BhdtU&Iu*t0!L1r1RV(+7wLg+A(NlJ1?<(t1*1@@3gl)#V;m^BwIU=iA1G5Z57J zzX$2gyEf+)_jAVIS!)Dt$%!+nMY*wE(_c$8QU7*IW$Qz)w7plnoz&B!Qm6VFti!d_ zJr0yEVB~OwEbZ?cdj}>(>6tc))9RpQyp;ud% zYer03Z%fm@^xbTJ3!d=!VLS#KU1<8NsFTAZT{wp3zMt8u=$paqktq2|kEUm+&c{}F z4%bYM{N8p-+Y$P-#R&YGbio=8iG81$KEk&>;|=%GaF^x!;aH<%7=_{d+?_KfYPMt~ zbWhOkN;=v#2-p>y)&2n782P1jC$uT_gRK?bG3kw^2-)v9#^j71^i&v}u=OrabTk}u zl#|biskxT!GjLqS*sh!Ko1{e@H<0ew@V299P~_W|&FHDnAJ#<}KWMO7i4XLPHJ0Hs zJeTP?gtMz7b(}Cet|r;Ui`?4jYw_9Xk2*IJmIE4aDovKb9Tj zq#zg5e6q}sX6Pcto|kn8N!&GyN+xl~o5T-Nmpi7*ru&q>wqucgOj1ePCjG>iKP@|S zKO%2f&+Bf6^)bKJZ4C-GKG%u-VEtR&1kdlfNNSI(h&)e~JM|$JQ~h%eb^_**bZz@~ z)6t|Utq)CAF)wT%jL##_Tb+!;u)gLT!^(r;_{ace)$7t)V=g_v2R~xzX7pm)wB<2V1ZI&bFX?$Qb4SnSO*w>l}g|4=W`LAj~!6wcMwKgR{kgoYJYKZtgP!%YUH-fv}6IAD6 zuNm1z162ykx3WYviLR+i(!lci+IQMY)(6@qz^`Tz0|!18__GVZ-N~oMJ z{|o$}uOrWj{IOAiGh~CX1o!*O;~00kBv-B9W|9`A>(gL$*)iQ2DNuD&m&at*eWi|7 zi<{i2xcow961gS0h}%d8M;;WS#Ja$3QUj6d5u)Va*Sj*Z54W@$#Rc1JAkaK9mSK8wFlU9H8F^IjpDsS|x^z4hHV@_t#+|BxTdCb{bFtvE*Z9dRm&hN_{MYSUr`wh>p`% zTPf?ugK3e>Hi3!WTb(9a#hRUWRN9X>C8u9vK0cQ6+;3%P$d^Q$TaAb(!oFtM-5ns`)M=cQ)0PH=_nVt%mIm%IR_3C1Oc)FaIZl!(J`QsKj}Yq%F5uD3e`GY>mJ{4ChaP!lqf<5iCss{+J=Wj2XUyTBf=^Z0^a^xlsa&^Te-#nhqV5zh*W%~fa;g3shYR-Y;~=$ywRp9 zXNn+t?@oxjEKu&<`-mN_7koJ9h$%s`k<8&Ovw&(x&JFg0Q$@At6GJX z*=mZfP+}siqCZut`ds~ya_9HdTgWqwkxgNws_c97L()Cl!kI~&N|+|7Ak+~w$#5bt zV5EEr{@OiVJqq7yjVbc9e8ywTG?o~ZcU7yI#d};=WD?VsHaZy9W$lcY1~@yDeOON= zIP>T0Y!OVcikcG;CJ!SIx*Jq`h*y^6f{mRSSVL)O$3P{klGnC`mtC9QI*Jxd^S41| zNsMuphuKc-LFV*?F}y#*+X23kjI=^r~oZfMpI7T0zn;a{HCf=9H8C{s*;N(LGFxc91JsHdg}c^ekC zp%aRW=Y3G5m#>?X%5AD%KU33y*6-?GUV5hKOy{|*`^^8^UL}m+jcDl~u|O1LaSLdb z#TauuMyiJEmbA3+GD_2lL;U-dZ1qr~yq3l1ihkMe;gnt4rPX0{^)yA9?;9u~-j?@n{g?>}G zLks*IRWIREcVZ6t!NwXerCqGv#H{sfOHIVVMIIdx= zvAp?kQ;ng!CbM~o{#M~<_D8*2I+tIpW5j$HZKYO)^^=Vvzxdgd1!TF~0!=xwm;9$Z zw++x-sh-xV61CU8Xenx*+L&kkQGJWC$+EI=4cpaRmY&4xF@BGEE1GDS8#Yebs!#V@ zujr+|yIs)?B@YuBWz%}TRs?JFDo?3*7st&r%EU*`-bM#;WI4*33RC?5Opg$zbpr)^|UtPM{}s zu?v%#*V-SY58}eDB{9#0ldLRxF=@Gw-{dPXx?AuIl)F@KMsSQEV{_MRCz(NLYSCumFdMBQimw?GS8_C zRXvKH;Psj%>XAOlz+A1+r3k{n^H>w}AaB3&J10SKlE0AGD%#srCRieAuN*4cBWuii zCAp=DNa>b4sQOQ>S2EStgJ-FiYS;VJXpe%R%M9>1oR0Qm_{ky^r`3fJ@DDaui6ax zcE%@Q6n7nKAMiigT5cSuEZ@N20XgM97DYpclAlOU!2GF$<&DUM;Q7ke$O)fy8X5|? zCIL&aKxheVEEX@@M^D0doV&~@%-ryYeGi*i?!_aq)wzH$0Nb0qP<#Ws8r>^v#XbZt zSH|JveXgi4;wi2sAPN_Q^XfH*G15nkC-fUQfsFh5ga$rqwQfh*dag_tmFq2trM4#X z#Y3o==&RCOFxLsV&47JWvVqVEVkpzzTVzK*I-6zKY0Ya&g+gxUdJ*^;&fs^d9( zWYh6>UVTgRuGT|k1KGP;JaX3a_FJ*!0O29?)##&=!zRDr|H=0lK;KB!BHc|lZ*4YZ z=i1h|&e|ezs9!T<0c&~VpKho=p7FOUt@IRoT*tT^Jujm5S@LLMj%{`H0ZF}u8+={P zF-`W(RW%y=xMkRR_`{$Am8Nv3tgpJEavS@u#=CY#{cFvBT3+c#El&TD;|x@?HY9!o zOL--c_aTn(Lr^wslFso;N46`YowlO)HFp4&>{e}*1XTDnP^>m(ans(qZK~Ynt0hm= zwd|MKDH;ubXd+F!N#qsT4g8iS1*Jnp3c8mPUaFQjyCAOsLUT_H(&vkZN&}dg%skl! z_Wjyriaxyhl77l6!L6)T)pl`X!edRa^m$~pcC$hglnh4OcS07}eFP?FEiw@rs5S`z z?gil+5yXGq+#o3tzOK0>eI~wHTqcL4N3sSh$ICY-7*!{giy{LwdUbbD0uZZh@j3?H z1X1Uca6N3V^l*Pj?g$Lp`cC6udWld0I@~h5&|5W=_ONv zcjCV&w9u9*Z&f(l7F4Cd5s9}iAVdpYBv2?8sPJX=LqhojISUbU(^LKk#>|5w)!l##Jfm)8}I9~9o&OA%BMDqsCB$Vb})rD zrE_;tsa45>KIG@ZR8brt<*LW-7X)%YC)@i= z@0B==&{h`Y!j~E4>2XLRdsysa3}UY+H*4Zl)qBPt5>SEF}QZP=1cf!Kn;L_W9;d{ex6!55Vks(Aob{A;lH9y%0JDsBtms! zos(>Yrm@s4H)%_A2ibk?nJN2KN-!y=Nc{s!2p_D~!6|{0!NEwj=XwZ4i=CIir?4V{ zyWlZY#JDIr34N>8N;+Uu=|EW|{4r;N{2anaQ7SpewHROZaI`e+uVyoPEierz#b};) z!6#U+b1z(g%lO6o3PR8D7B&z|YI?;P#OjhBX@8Q5-_x;oA%CcU z6STle?M|Y}YXHb3SS~)$6GF;c!y9aDr?&|Ljj1(4k)Pp8Nv!yzVOq{$=~?}vlo9gP z`hTXqQ5tn%*f%wUdLGEtM%qJ;e!zF~luIRKB@1~YIiFh;^x^zK+nSmI!YJ#(;u8#`_rBOq;AZJ3?gr?cN z>W+QY8wJw#%;GJglGc^k%O!O! z7`2cGlTKxgO<##`RrS?6;z(U=u_vLV{g<_kC}PY?QWA;m^5}O&9)CcHg%FE&`z=nJpxDMgc!HFYa7kN2`{732Xv@}n6npjX+Lum{|v?i1AuSu~47l3+fN=l2)M0>61Upib~I zry1xiG>Q3-vRg5$o}t>T{9e97y-OXRe@?SZQ=KtL+on|~fB*>0iaHIdz{4S_kOq3^ z?+GgqPmfZBjg~n5hbCc)=4knKptEk9(g9?ZyQ$J3&wPfu4BDH%Mbik^C!}imNWZ8J zfF9Wpk`69KoBb1^eVCI61)s$(IR_(qagqVcN-=Sri+nS7xNNE72lh9Akjfi(Ntdb% z@uY)GOw14c^Pt^O`rBcao?I8e?--6{uKqNpO}_~RA?KG zS^jOndHq|@K~RKluJZ$!O94&4Oy;s9HHpR_RdY)^jrnz)oNoqoqjPG7{xsuC>_hz& z_N*ykx*hzgphdbOv8#`b`YL&!8nvNH$aUZ+6 zMtj+9KE57ubzF)+Kyn)fBQhacIS@H678kxkhe>m?)}b--LCGX4RGf(EL6@jnBJ}8g zn#!O#STx}4J03HFpWSX^ci}^hAMhYlUSAGr<(U=1&>A!eTIHLJZbbUIPr&T^Ax^271?#Ho0*-5*mR|!|+B*fm!S}$S z%&!m|Y)SeH-G-WCGT<_JT7&?;jW`7DLiorpUncSmec+yl>al}P2COgMQTsxhk2IH; z0R52{1+#z^XhY_Da0gIpGL2_8mTeCNYw zNs-+$VsL*u7mBBkI8A z`naIG5TTpryA7_U{<`l&0`1OU2jnh!sb-1tNz3rE5Y<8N3Jnj52o&3>H8p)`{x|7*Y=<0SEEFTEjMKF?*XUa78kaMm3IK2*)N zzOS8FeAPOj!InGHGOFoSTAX60uLfDg82;)oYvXh4Q7# zCtbF7vV)d71f^E)GmmQ;Q|xYHH;>K%jGI_rQjZwkbGO917~%zJ)E0fKxGFSM_f(b~ za8;M7jPSZoZPz%sMo>w>9fwoo9avfM)G(X%sz`1a%SE!^>8<>CsU`ZpqG@r*b%?|o z1yR3cy`c^it}F?-PYqY^_Z~nl1H4>$WCbK}97T>rEEWE`M#193+q%A@6WJiOL~<(S zGnFozAEzTw2MQ>5gwph;28WZG}oJr+o{^Fz4(4q>bMTy zf^8~$i?3JL7xdyQHS@BL;oaJODNEr98#~rTO~%uqjiCuSaO}aZ(V>zTx>1_DEUY&mt8K7T~`37;;&q{uT zciyGf;t!()IEFgO*c0ZVS=1l&celej#_4PJzx@V)Bi! zqxpK~UO3mDeLW1Ot#w8x4Fdi?sEdnIa* zl_s7d$JuHer2a5*f&Ml~D`IrssGXc6=!k9Dl*VscL3c|iZS`XErWV_laSnyIT0;bW zL5IzI#B=?8O=a@&9uJNEReM@u>DSwbIM zf9{T{PL>^lS>Z~vL~IT^YVwn{`lT5DQ_k=lt3RqSx;W^Lg2@g!)N6Qu;rNyg7B{=h z#^6p&HCbc%OXF8r97X-3@0xx|WZ`AThq9MJ2Mqg_D!(fIa`j)&B;86t?ovWsfwnue zQeMc>0;!eDKbdvIQZI5$4K-IwlH<9i23dIYRwGvt5^e`ss-l8U>(!cUKTwAPwVrBf zDOBOoMSej79Bz>;^lbiPGfVO;OJnMgT~2vx+^Oh}PcYn6xkk@73{r0lPt#A;<^?^} zRRHh(R!{)cZD)h*L%dvWlA)-@F@V^Goy~`g+m&~+0*nLI4JkVe`I>F<6ZLxSm1wK( z67V}bP3Hv#2YsUIV26nL6PpN^KG%AOpQas3w)akgLqyu#(noew@j)WHwHPp_@Q}CzM9RF#!l9GB! za35-_>jHc*`ONV<_Lf|hHl}--W8pC7G)9c+&~Uy72e#!4fPkzPcgaZaPHTVpJag511{=pYCD~dBW?rn#JA0GXvtw*>i{AtS}Nm_8A^{wn- zfZXzrvdjCCDN&Q?-e#Bw9CFUpCqjQ6Cg@y{pBY%^asJ5U1s!3+y0{%}HgQMv!IrDi z<>5DN0rIoK5f)teJD}ZkKojpX);JPSx}VdxL3-ylT`f}M&_vatpE5?YKM~(bZfX-s z&&SPec`9ESz15bfYzV(+*{607&NatsdhPRF4{*SzNdE`2dW_R8Kq8%QQZ49GhxOzd z>}mSlwr~ZJoX~PtH9AgjQ>cy6t1K@ycfuc;2_QDO(c}Tn4|rhcfk*lf`U!~4qnNsg zZgX)U?_sAL-jU<+o#{tgHfsAPd)UN4Vw}+G0NSz6=A}@1_)}9ATpBDljzdZV`|FpX zl|EZ^$ylL>g>t|HT}sFd{Dz}H(L+enH`*9*V{(716Ven{WIln$M7NmO=$-Iq#z+hb z)*Jr8Ck6)S_Tg)No>5vN)Z+*#C1$wji2cMh$4Wbm>YsGd>VU6Lx^I3;RK`V{mJv6i zF=IIy82;Q4Lkfet^}*zdz-;O(dCqr?-AVkw;}h|iyzR1qC?YpG!uS=kGd;unNOv`9 zm+7|dXPlRDmws%t(xBE)41cZ{>H7sQ)@gK)1L;(iZi#OcnW-!CbSC6fuggXJ3&nDr zgSU~tr*ms63+5T0)GaT03GrwFl~Lj^P48<}j5*C`Y2B54_729@TrXZPdsAXRQ6_(0 zM7eZ>nD9?jI?DZByEGcrlg|F-|5RSmU#R?An-46j9p12Cgf}$Mz35F%+n8tthv~$5 zm{ZBwCJ0QB2xMY*_+Lr5Y=M7({JwIN>v6SObE)ll5wmfYZgA-hMw!;XlF1q(OsyTu z+1^yqaFbtMZl!M${>om#S}$pdf6F^0cL?7sx~N>{KVEiN%F)d4JO^~y`*jwj7U z0enC8%Q9zSupp(%OYGB#*X2q7DZAZhlYPy0Y`&|w5>IhsRqMkg!cI+_|3K*xKGyTZg^NF#-dWs|!PbRlMfX<6ut1pDGl5P`a}|4^n28 z(jDPJ@lLGQ@SJcKe-{$xUnQwW-nv~->_wNDpQJY96VMA;Ww@haPyRG~0(W-FzxeQm z#>zg}kJ9wI{@By3p-ls^D{;SA!|b#WIe!p#-rpp?iG6gRq@dvrrjyBq#&yW!43Tl7 z{7bIEpyGTknxQ{b|FC?Qet5~pnism#%!iE=s0DF9m}%t4@Gc>nl9_W)nBv)rwI> zv+-MEXWMurH0^rJ4|#o#v+X!XQP^aa)*H&!T3kww*Yq)+$h=3(H!9+tn`aul!q0N= z>Tddf7x$x9xNno6Cv`?iLU>OeygJo7eXi_b)|<}roM8ox?KkU_O3$_SmaME!vVF^Z z+_20#Al~kqHvb#ZpUX442ZV?=>XY1i<+CWjI3eC;-Xu6P1)Y6MhGc%3DPnKWw{_pF zA5`ktRZ!AiwWR%E=JN*k*6{cm#w_cCh*0hb^VooDk=W4VF-9)YrP;>O?iO6dYn%3# zj#VyUoUAP7_Od3_8XIysR@&l{n|x=+fy^Y~PxhhMQ_`XQ9iitGp5j%$Ds`Bg?$QQ? zs)Nm6YK~UgP;-4!&0mER?M{P=vxL5`=~aCkb2hWI_z!0uXHP~q|AAmsY>7BWOb>09 zZIW&9RVx1~ue)5*hG~ZxKUNeqj)SjPXVA~dw$_z1&1{Tj=A_o;&>!;N7ss+ngiF(Z zaX(1%?Pp2rWFJE-q;%z6UxmGs73w-tQv{qa1eT~cQ=zKzi@Xx4wmMG0W?^+ZM2cEv zqe!ACvNWg4p*jnG$SH(hgGQ`APNS+}ClP2vxZD7h$ICTXl_ zl{+vyYibo>&E|$i<;B8JbVBW)KAg2v1IOg^zH6_BP80`&Q+>PSlc4#o9jacWA4SVc zSMS$;EGpFWh{a`%T4wWsD#V^48B%u;cvJ{AIfC=j4l_xpCMJ^m33dt%5!Jv?eJ{u? z$bQ#vDh1k27G)nnqqPGHMx$)eyb>PT);y%*61uKhU0Z~nDjY+*jXp`En&sGl7o4nvt#>O_jlzBroXk0Tqb4%XSMMVHuV|NUB|}=?pbMz(Uo)IqQE;;1 zDCL{h$k3CkVmvu{WK?Lgus?CuZ<@4)z}#ji_YyJq{PdHyMQS9+X5A{xD=f8aqkk)N zGtaNuSoPUtD417&#VAUvrSCFS#3Zm6>61fe3P4?ipHvz^O>?`VRFj3+`?QL#Zq=jg z#Lh8-lLZ0oFnvbpxYl!3(y9?Ho&{L_NUJQZo<7=qAf}Ez-ZVV)lwhc#-fxlQs&23Q zAY}p-gV9sNXXmPzSz$9L2paPvx^L67OJcgZs}d>;J4y>Ub%M5k(r8U)+wm9$YncTK zeaGKmiuOAx=`!?n4^jRfduJ6DSGMo#bh>-D>~@O~cO^myBtQrb!QEY|iYls#a#!5l z-CYWIch}(V1V|tu0TR|-N$*vv0of68jm>`+P6qC{g^xP4Xe~Izv?#{6BHu7{H+Ruv%k`t)D5z zEDVvSebI6X*{1k8L=-*$IMucW1KcMe_3@`~^xIDorO!{}uaeDAAW0%r60KY7mfbT< zt1c6Thn5&jpm$CDO*AnyEtL5j_T>|jwHw~1g^$@=>?ly8Hu4z6sQzi90`QGXZx#;oFP(DiReR0H5 zs^5)Q=+9~H7nHC?bdQr4iKiGY6o#q*%@EzJ`713B@`cV0ZPoaS!4mzxrk2S9ovXmJ z*r4}_Pe8tBT)uw~F2HcQ`4dWxv2lTey~>21BoPXjQ{*`1+gu~`VRdotk2VI{TAW&= zXng_)r?F-n&$+IEG9O{jiC5d4WhdT$Xp3aq-4sA3v&AnIVshCRPgW9)*gugEE84hS zK|xewT+OVzHD_GBjPC2*aL(39FmiMnP>?fQbbKdXYOU>f`Mw6M$wBL;CGs5C`NAqj ziPL;?hwuyM7zv`V8TbpX6c>QWPtNXi0f(F7cz+FV)ni-#KpLmH?h70sQ z946-U_NJFzfn&hMqxL%v!l$m{2RS>$Uir!3`$(v=aNsTL&()LsZyLtv-0|ht5I5-Z z7FPJxl;9~XUTk&2UEw|x+U2TrGsn)vS^J`>eUT&Llqx=nn@icUHBik*q&D>){L1+f*2u^+P?7FfXPA(gK22z~Z- z=T4I!;7{=#rcy{GqPw}H$(MFcmThL0mhm=xmMr~MDA`6??J@XG-BH=gP4 zvRWVtyz3+KT0A}+$2=!Vx=U2%GoXZ0ej%JPUa>|aG|ikKAr{e=w_uI>|FB+y@2Rq& z*~n|Mci;|aW6?Y$3lktWYM~$L)Q|q4&>Z(fdm@Wf)60yp?`%f z*1L@@vQ;!R$EBJ*GYP{d>V|{+7jsq2tuGQQWGtaXQoHCQ_&9l9Ko<3c`t`LAOg!!O zxgmTv-Qa{VDVssX6lgC}I551vI%NbrCZL8YuKS&t2(?go%d&)aSLP^0jK&i^X4^rR z5RgNf(MPX=vjqn2{Lgq%=J^v@Bnf7N{fOo&`wFx|m&1Bxnre8O)vgm_(!k15j>um2{}`#LYtau9>;3m~?^_+U1PME|lo}g#ZPCb6mP^h1PcH zJRgnwmRodEkEqKjM@y*|2AqN@Y6kg9nt&HI_}FXL7!tiQmA^9$_xwWI$FkksQe+3R z=~^k^j`+sq=yf*wGbiHtYHYK^$Voc!0vCZHp>L>u2F{2+TfKE z4@Wh5Cl!J3w*2v-9MJ&s>z&`oU%`)Gv87a^uAQ*}5BR=(tc7uscm`bp-_{ri(QMK@9$A##8 zq*l~L=y%51)@`t5Gd-gSc&p{_nu&H<)@x6yQE|{_$=~eb;pq<~u&qe&Y9GRv_P$r_ zNkTZ!Gi?+VBJp?-O_-u?H)G{%RcTdeQ)@G6SPb2T{j4zv7l9i*`NNK57boG5>P9^m zUd4!Gr#!g=T+{M2nLtP7EM#hY-E?0WVNDTHdmCrN3gpM_7D%wuFM51VIV z6$~V-a5yvd7|27sufj#!LxhIMYltQyARLT}B7+VXA|>=nC0?Amah5=wr$6S4qDU~f zw$F^xNktZtrt##f`WY6H@Fo%tU(7D7UHh?sVlV3pg_4 z?ActRGUFxRD!GGMY5SW&C-ZCb5aU_Kw*Gaqea5z0i`7}?5AseBC8oe*CtDKJTo`Q^ z%Z$J6hOTEmzxqA)DNF3^JfV~2a^eOV!Rmv-^pzZMnSX0!>yW2cZW`zyrpB`9;daPt z*<9tK9>ZYH+~dLs#3-ltb`<&o$LH!jtPV%%+;0R3=kN(j@^N+@bWV50{hnE{;d!@* zdgdn9u8-AZ%nMxP<@v1zoMDf(pp{Mm!f=GJWBu(MRIJ0xt2&r7{JW?E25S(4zQ^lL#BXnuP^zcNeuC?km@tq+|4om7ZMT(ywS6R~U z`%p$9PW-oMzr%wsY0&jid8fZ-Bx0+NU1W|DtDsSsJ3467CG2(m23;-OHREe4OZZ!6 z@v@!-6RQ)CY>DxZsQahM-@-5RAEr1W%P(2d4r6pr%g|}~iDMp&L9!Y|AE{+*Xi|YH zG`plDZ-2^SPvtX=kM)F1H|{93QEUXC2`>?BAQ~Y{Z%&Yp*tcA=r*z^*Pg7`E;>fW@ z`g2N+jR5?gR^J+TA#!buw7EzdC{I}(eH;G0j0ENjJGdAIJBjKRbinsv9^Y&rcH+t} zG03Gv(bFkZS91RG`}9k+pRJEVPuuz$y|Dcn0n_S797akgRU&^y{~_IlzGDAO>;gs- z+ahR;v%}}yY#>Au<1fXMO39(%9T0_7kK^t%65ZPBp>-6x+vq-|8FNWX1h$W*D#;^c zaUIfRq%Yxu=mvV6$PrY+3X-1R9L9T6pqKiI{nY6**U11~>-ZdXfx)wcSaJw^hMv~F z#A?lA=sl8);*4z;SyP(d?keS~Xf-O4`nBLA%th+%%~f0+?ak#w#2@I}XKct8jCH zYXn>7_L*?fBn!@mq#kCCS^NyHdL1($S|oAGG-j=ZIKGN%kTs5>RJ(08=bosWT`1>> z-~+TB=PUjr*!%3g%NqE3_Uf4y5|-V~7f(rNmzzhJ)VcT@{A6zAyrYBSd$|lN=39eLIuT84GshRH!#@_QkQXf3OD`CbJ7X!KlnN+RvfaqoH5bsJN;@i zNslQFRm)%8bDwNkFS*gB{Gsn$twm9Y3oZtN;wUj^MSd~xwvmUIeef3?FQ5I9IOy;V z-wVoh?r-J`GzZl&{VH0DW}|vDtwp!u$@jE5!&*rNx`b)V!$Nww<>=iVh7n}_I*j=n z;`0(eJ2#i_5Dbb`*&7c3icmnj}MWBi3~4!+8CQtb;ufW z6MZlAP;ZixPvz3Gs71%-===0UqYwlMMb{=Gzp+28qKL}Cu;sq7hhlfcSFqRc#0Nj) zHVFH7&lAc>0oN@_E|hy0zoy7idr!TgZqjXzKcrhTCJdk3f*(J$RuHEM8p|ybtm^UjMBzZ zJ6=u0v*rvsAV*pLnmf=PR=M&S+iliUSrNOVtZ{KmlmTl?xZOUU{qx-`*j2XZ^<2U= zHteD&NuBL^I+$X>jyb+XJIXFJfLQaK4r@k1@*InmhF~p@rm{c5cO5pw6_F+mzQT3r zP6yGucd(DRE7x1_(cHj`xx^VRcoGyD;56`^q;_zu4bEE;-5+Y|+r+pTD@8$jT^TZK z@Sk16#D(m9o!f=W(dV4MxhsUpb&|L~hQH;AyVyZ=b%;JaPA=pY^F5;aa$f0Aa$cxi z)o|bhYkjY%&AG34SlW=?W%Rw69y`;F@4gc|%}VfA5W5Ddb7g?NhHyCN!nuYnK9R^_ z;3kiL!|5hX=ru48>zJxXgJ&^UJPBg*84E~dGmn_Hh;}lStxWD;VunHXZrLz5;ptZf zSvn}#IUcJSd-22yTZzzm6vA$xNa>bQJ&dB&2C4m~9tu0Os}>GYr)fUc?xIHYqtFP! z5qcK9?3N@$7Ww?jFk>DgaPBG79gjYFi6uq~I9kFQrMl_#kq8!d9j0JwX;l8hh&2MXCl~<;rWC1=0EZ6}la{>|{6t zOPx8U#?+=yY2U(XA@8d^$H|~(=4wYWv1mD=?T3<%ELpRNXx z3^=bZ9HIQm={l88&EdEoXVJgoXldSo+qogFYTZ>)?#2x425Gg)3^gm}x~wZ##4K z;veNYWf|oPL!6G9HQv>6jI>(1A>w!q_WLDGhdMjOGhGfSdoo|JgCO4Jh@Zo^WMj2O zN2U(#NwmX`!57jt4rJq?M?wytnF|X4z%8*FynB}G2xZ^6!Bs&>UV?H@qrX3UnEM&F znQwygj;McRl><<+)V|`}HZoV(V~?7GD_Gbu7V%>3Y-j5dp=!1pbmGoSb}-`S8{k>T zD3ePRc04BUEI+#x|LVjE_7eHqqYpU0QfF0fFz;CKD;P3=wOWT#gI%zTM;1! z(-X0Lr<*x|iobD$^%Dkp$%CbW7eA}R3MK+4G+A4e(4!2tG;L3XP9s9PlsWw|hZ zS6-kTw>u(lNx`9hkgTCBqt`??s7y?j&>h+p9OX_Ct&gC3Ly&Gu61+4<2Pmh`b}%BS zo8V0r%JhU|e9T$qb!9r~2aKxxx1=C!mn4-eh<_+rLw-T<6go<&C$8SfqlS_-ZphPg zDIJ%;p#MlyIQJ#Jo6b9Vf|0=3J!Zz#Wt~z!PEaS>$dwSDljz{Vb}3|*Xc&n@feQVD zOrsj!$)`|g+BZzV`d z`I3XnUiM8_(^wIf#?Pm#O9O1-d-qg0*y zR|XFrS-7qnF9~nByfpV0V7Z*NmgQG*HiCU|^}LfCV&uXV#|m`rX_DjjINRgv4s7Dg z5r)Gr)F0(hF8f9&q-0zUo1!0m<$T?OC!FTQwH_DHaomI*=a+D_Mc7=u<bfRROLPCjWvpX9PTrWSHYcvf;Dg*ikl*e#_COi^!eln%lgQRNxH5yv5WwWW*Q3 zS2#*YWr1%wx6!Hm*Eqjpf4usHJ%o3@;L8pt?wlsr?7Rh*xo|mX%Ok#MV8;nt$vCwnI3;g)qUFJ<96gh3%6#GVCyS1Ox3f9((26>k5*L=!u9F}~jye87g+HeF zXc~fd;&`Q*stO|q_*QO+-|D;kr=Kjoh0tzmSpol#KYAEx+sKj;ljxRC!Is>migJmULu^RPP>_+8DVVr~`b+__fHbBgk*cN_znEZ;HCAN1{`g6t_K zT>P>V1Qj8o;23W2DkSePjx`bZ!{H|W2>%4PnDF%41Xqm|c4>|iPeGmkoV`sGJT1m{ zpttiGu&tP9juODTW~3hNJ8*Eni12f#@fku7xH80>0>5%{NZ$Ob9CPyewRfCzRF%uW zvcqX*=f&B-F^-&Ouo{`Ne1WW1*1e-ytS%PtXn@m9G7m7sdvOFu6dHg&Jyt(P{=~~uFRs~-NONK3cbc|)qj(xPmend+V`H}UM zULtgwWzR?v_@4D06MpL}<~!E!*Tq<$vv`*Y%yzc)`4VOzyZrP!rZ4APzIV()&aq>M zSwC{Fh@W9TXVr=DF*DdYLVqyL*^>hQWd6W0y!A8lC@1^6HdC0hbvc}QhI{t>BJ&&W z-7`0sXSt_OoM$pPy~oZl+c-go4gtV_VP}K8a|rzT{EM@P4t@6iumAY@|NZ;3!=Hcg zEN?fQ+I{I_o3 zxhrr_@V=1ngNGubVvochOGrvd%gD;fD?CwDQdUt_Q`gYc($>+{(>E|QGBzQ&Q8?GcvQXb8>lk`2~eV#U-U>19S=jIm{mzGynpRKKLJb$sd_43vB>o+^QdvD+E1BX8Q z?C{~wKmP*E;2&Ro_4U`^fI0m0yYIdSqxkWspMD0@`1Ln1k3Wupi5xo)W&(x+rUJ$S z<^l%uKA8{E{F%-F2*3N{sN>-+-P+NEW% zy{pf_7O!t?JO>-SxwZ8YZ1?u-*KgkJfNkG<`}WN9Qs^GHfy;x%VE7a)A?m%ru)v{ z<&(po8Gg#)Qx5+*mOk}|PyOLjfB1jjUp|eCPvhdhj|-U(pJCpCpJCpCpJDcL%w4y5 z7GclwEK}zTtO}hTKemfyOE zQJCZK>hJQlK<3=rLM4%R`CtyYpaaj;`DMOo@LG{s(o(4zZ??+3e6qo!eyGu=qq`b8 z)LM?4Y%HP9*A=o?s`DH-a&y^Rf0sWU6hQ}#4-UI|#!jyaje|ByP2!iUOtR)1O$$fc zOlrFuAYHB1sG){(!eng;ZLX$>y}{`qT?M+DsZ68*IC4Yp_qND#>G&WsK>f68d~b5oIZ^ zkh1!B*(;PgyI1rthdh1E>tbEjW|fY|O0#ajWS?qcPoG9{Q=3^sZ3CjKsunj`QAHUk ztDsNhl#yqWN{I7OMZ~4Q%WlyJhlg(q)pYg>bkVQMv}x=0nohG_8oq;L(#fs8TE(?( z77dk+cAe#Q`2MnL%1~Yi<3R&`}fsJ7C2LQj4zVIaL4Hxyrq84Itlp9n0+O#NMUib02x zClB_DRCQjLsX^BoRZx@tvb3HViJ*p2h17~Z{i2c%o0_5)baQT_eMee7x+lIC)gMuV zd>UAV9Pz0_js9JBN@PyIDOGr|TcWJJS)*h&+pS>UH!a3&T968^7*kFu8ZgQ)=!RBg zwZrR^T5MaQn{3-d8(>}j^{`&=I@^BtTExKL<#nm_sn=!l!mrB})t6gk)dwepZCX~u zIh70Y!G#m*iMc~2Ica^C#j#zMm7yJ$wf=3Ejb1I5&2G(BZB9+r?GBBQj=#%xx#Y=L z6|(o&>t&>dNA4=NEkCra+LYoHE-43OPwB)ajT)py3>xGH^cfa;^%#}8b{SPVbQ)K) zJ4|XB?Phhfc8mJI%U0!MzRhasdsBVFcRCgY)M~aK!Hc%!nc1rv-U)M>VIh+m@!q4F zX)Z%rS)4&_9;06;pW3HeNbb`wCiEJV;ChWp|1QsK#EvbtJvi4rcSERdQ%IwDR|=N1 ztxQSSP;m)fQug1DkzhTc^|t{sL{0$Na)LmXh6<2w1OqZGX*-$L9-CRv@Ml@@l*LR`{!9k8aw3h= zG?L2ddYbIe-yh>R+#BLD(c|Me-R&JT+u|QSTOSlL^H=%g@Mm_Pa`=?Pe~zI~{ozx8 z_|zZ%-}eEWeBaD8hV5sWQ+Km1-L`V9L)UVl zNlUr#+}T`Y*<=p3ek_~ZHk`@m8BFIs?MrbU>yB`n>GTU(tPBlVEcqLJaLD;j2Q$iE zj)m(>o>lOAo=w7X9xQt+#26ffFsB{e+oS!!6|>R$!O68$fSEMwTa923&}`TwhY^ML1tmNBa(Hff9H zFy35^UCB%{reeI6R6p2A?d+(b4_1~iCsR@#mctVQ)}o^RIv*Tz|CPgghg@UgZk~zb zcA=U7ixP{dwF;}`cbRub&zVCnyBs7 zwTyv0aPAvgOj!&n_E-!m4Ef6d+2FIt{g*?wA>?hIA^uIFA?H=8k=OHTli<~6%gDK2 zXx#81G`p(@Ro2voZ>VXcbe7lB2Qn)t;~^!axsVd)xsX!7*}n?tzz6UY1U}@DV_>tF zuaA9Qtk2x4G;mvMF!o#MvI-m>dvku4#rgCM*YiiQ-0-)sep3l$-l~bhyA=i9p3SDEq98wkz1A8q}3)} z#!R1{^Wdaof0^~m8 z+s{`N0P@t8-{xzXy)M(R-Kf{JpX=7a4oxV!cg(0p)eY-qmiJl}7j@XxXcSQQ*;x&J-7kh?NbNubE`!TxYS|?U24hT@4v==p7`;% z1yAn2%U4x;U8bV*tWimS{Hd%(_q;T%Vfl$~*_3*0{;*MIZXcvDxeHPm*=|!8*kaS{ z-DK0|+6d`%1Z!p71{4VPFY`A4@v*(4CwKOWloejoJW-nNm3`7XD-La3lVO%GDf#71 zX~kxZn5HKTnB|4{n3wu@nOAwVo7XwFSu}FMIs~)DriIaL+e&Xn{$+NHB#-VE%in%g zp&-7{E+x`GEv(kG_84BiCC|!VR`boA(Tj>5*Gmo_($DrDFvxf5H7wzF8~ zrq$#Q^BQ7@buF<2`j>fAB6Z|VsT}{aMu}U4<9CExo;}d6*p{-(-%?^`u4;Kj&ufGR zOsU7Yk7=ej3~OaF2DNi313G!&dCK7&F`pHY#0uW2#5*Wxd;T_*ASi)x7rqff7& zX01@9zuGJz-1s2w?i-=-Yfc}>oZu`K6D z0BaEFIfZbdvX%zUjd-minwOjnxirzgG z&jh43q5%mDzjqQyr`Hl>+NJ~>w;|9u2V+}7a`*PcEPpQjxz;iGk)v7=?au_Fb3u_JkYv7-m& zzdPud0+}X={cJPx+guCh?ObdB7d%+h8V`}Y%tPnQ^KeD~^G5WqexJ#vR!*d|8wX>Y zx>^G~o>qAVjFostkLUTtjAwyl`o)YL6d(mmCJlT#?~_T^F#)oS;qP)x!HR&nr5n=2ugj^@%EI+M9wwGl2)^ZmTWvwR~b)BK|*Q$Ujaqb3f@ zpAHf?fi%z|MaLM(Hnx42YXVjT%(!m~EWEdhY(h6mZDXHRAXAoWu~}2q_>%r2a(z=K zv$Hh8aWEs)Z6YopXeK5gVmih@a_X<}FNYf+98%vq7}>t%854H%O*pTM%spR~S_QqR zghf58N5;>$*k_D3*%xHDJ8dO@aO}P zDeQ5NXqQ>{*zj4Ou!tGo(1Qb{g8kvWLpt~@GIR|0a}6MS`3BfG#YT*mWgX}@*8ogj2m_?J~%+yz2AZN z4w*{QK#q>?UV*mxc9{+wtcut#wHx6khfQdMGsa%+Gv-k>qtNu?eq><|=n&Up-xAP( z?slog3_5{TJC|Db0oS^)e&^b#frA31gASRJ*MTh1fv2UlQ>uf zvJ+bE*9vWNZGyIQK@WBVwu{|J@8mSPba0#84-AlT|2H68`Z|!KBE4Irs`R2-MQx!& zMRjOG&b(tukyyK?=2kMN6P7b>oRl(TkrM^3Oz;O+Cb+d*)pJ{|o0-kf_f>#4dNZMw z(ZXtBv~Uj$kOeyA%3RyeQ+fQVTuFMR`H9rHz=fml?hU}NjbjT zyaL;0U5V|o1+lNdc4H3=zvt42rd**~5)xUV8SNcZIF88G>BjuT< zNBDwzu=liDjPtlg5)-TfkcYIh@q@ZN@ZOg^RKHOHvfs48uHUKv(GLTGADFiVB0s+= zkv+fMEPA4E`sS6!4PmL$9SMWn{U>(GuT-dE>q;)(%SwI@b4p>fY2_HggldBQxOy^j zOd}OOs+|S{vw@BnWI#qtGHgaHKr9c;o8m`5Zq$nYIyiRbx2E+w{AD{2Wq5#$elnn7 z8~R3$=(Q=w;jYQKQ4W?CiM8qCIWN+ZT%N;}qkN-x%I$}r9h z9K#3YWx3dQV*|&(Y+1d0tm4hR+j+nv>104sClrvd_IxX0&)JrskT)ec_UlqEw$Efd zY*u8wEtchd&6b|{n=B~>fE9@VqeZnq!$pligGKEigGHSKGv9Xhv-YK9-&AeiIFSzs z-A)EXBtn6QYM#JD6Arldj4VqxA}k#=G0 z!S;zO-h`|L7h1tAn^Qhbajlso`qWJlgX<@WQ4N!%#CmX>Y5gcIr@oKHtLx$(j86`K z=JhFuPdWVO*!f@8A7a4QhY0ZXAzIfOh&RCk30AC~1eoVmtX=4OI67e^0H3+wNiCRl zWS39Toogn^-VGC^;Km73bmIgisd0ju)i}t^Y3SnSG_*M$3?K%4jf#D635byv10q3( zI9)3s$;2K=v106|z}#OZ*#&RJq2pG=vFVEel)PDYR@oHSsdj?q*)&cLY8j`*w2V_z zTb?qrn!6mbnwy=on(Ge82ZuP&Ar^Fq0v!@`ErC>%_mvU)UOLqERT?7jd9r=Xvp8(( z5_mh#oFAif%GIG}g5}XPMh$8kqsF$4(9_$xxtXoaE}5-0t{H6=2LyEZ8F=py_b-Pe zT?-)H6s(L`Qr~9SIB#dz`oBm+MXn`dlb2%&+4G_F;%RSg^@O8a(+EAVeTW{{(aTQn zXm(2PsBum2D0NHgC~`j-KupA0db&1nw}{*|9@YDAb^z-_Abxb<5eCka3c>HwamjN zPG^y`1``>jZQ%~})xNGBMQ*-*d2W&YIUY&<8D2^KsUXQSHfU|cfw$tcj7?o0r|H>0_c#ast#o88H4kG=-WJF>`s9heY?oY>3J#CYo!7i zI8%j=87jtPwr7)ytCARXc~RWe?s3e10o0-6Bi;ZyX3k+}{_6Z0b z@%4`#@$-!v4gjll2LXr&pG6}0EE2&Sl2z4!Y(2wwdHU8ng@&kCr6$A|l@^TEdMJ0U z749|C4vlKBvrDfo#TMo9NHxjn^wzL=cArPM)38&h|Cm!)?5JB{+=xd&-0(s8Zw{%d zsz8pe{$9SGGmwA2oxPW4%P_VrpvHn!NNmDHdMGfHsPF+6gMUpl?l zF@ZhAjQ1X4CB_XqMa2&}haC*i;U^#o%pnQPAx%|zpQo+6Tc~UHs!SicQDcN$ZndzV z>9;10OqzIf3|mB0^*~a2Er^1|8gx}iDX!TqkJQb~W(-nuyap-x2?N};#C~o<;=n-w z5^jJFBJUkUfebaJcX?XcZ;Ew{H!JllSDTD%X8SCyN2g4PJ#(gR4O8afMZ;DpsXdUq zh*o%|cRjL+Q;F#!ms5L*RUW;By0{)jacVa+C#B~g0EwVOvd9G>SymLtR#Vt3&{Tg_ zrlq%5r(-nVWoS4$ZeY_pZ$fHWF?X$)vk1)@w@Qv5gzy5pq2L4`*x09Pw+bGrP?FZq5Lkj4S0y^;2W#1HQD8H!IP+x4*QXd=9)a#woMzpNzGb&e& zyz*vEBT~n#QX&Sec|JWhWe(uV1Zpe1iQIx~rnIt}sBK=2w6@3wa$92KK>(6&{s^Rs zUI5Z$Mc?MDNxmvqm0fL8m7N$=k?WgPHfedLfvI|-!_8kd@JpLEj*cEPP4yo%l zmohsot4ZyUIzl_Dp3qLNBX&5{k~(~9$ep3p2LVU{9n!?k16lIIJH;xZ8?}m}Gd)j) z`ln@-TUHgVs$QxR3pTWzGZys&W2W_E0>=zf-JTldaQcjkDBY%I_%5sW^ZqJK7p4l^ zMXkhjJ67PjJjM zR;gBC4by52c_4t?yZ_uSmHch4ZD$~|wv&c9&6F060? z8C$r3N-bDGXBRBk7Zyz6O7cgDW%&b?ioB2Ilf$1`eahi~aSnmtxCjBqMJPBff-E^Z ze&DzOPh8&M;^J1R)U;*X`*Goxzku`u$He<_5%+#v6fU513K#5)3MTO-1w*9r{66Z( z0|bL(GXxx)!H@0({t8M!h`Ip~p$h>bjd4J@C2KbX>haPK5&Ya06Z?!!PF*Imau%>I z1@ma%;yF}U$s9VaWX?XlWFC`OJc%zV9wL_(_0UU;+8G}Y5CS@cf({{|LjdRyuC5Q( zLBKkxF$RdSVC+Rg++Kws0$=!`qo27DQ;)3@Am7Idg zj|B+*4~GziCqRsv4v+}eAd-!dK#B$BT@u9cO+4IZD-s#D9*B)!@t|fba=7`^WcRW$ zTu{{rKE8UGm{Zk5&97=<=2z8m3aZMv`PIb^AI}Gea4?53Fo!VEAx=#jNCs;VX(k9D z-IBDQ2I1_az&&5aBZJo?FtJPil(cC#HgA~aTGmVUtLY*|)pk(QYFp^M+FCZRw%j4F zw$PDR%X8#`hZKAqKp2=q_}$YV9Ksb8fOs`6AXOJ!onZn8SBT)jS`2GD-PY}SDk@+l z9ve9uK~5U=W99TZJC(LDJ!>0iAq{o(#D;2CPD3d-x4ytBw?4-yw;}TY{M#WM%pp=i z9!ONv1k%Czf0hYsKieF;lWk4k%7QtsWg>kRQ!$~F@#Oe{5Jq;JmqSU7qg!nm%fG3Z z9n)0A$!y{|W;bOyXE&v}WH+T8fDaB4U=Hscq7>wSWHk+N{%^FOZ47yvV`l$4*NXBy z7s_49wey(E!32z?5aPO{Xju(`?BY^)XE1ZG<}8P>)(oeV)->nL)@0YrmIRPE*Q}2Q z2nV0Vdxr?nAzDEeNKsP*vh)qX(;=*Pd8Wvh`If}B0tj=l5aA5&kntbLz{a*FkTa{o znZ@~j4mD|RZY_x}f$edwaqY2g>Fv=Vk!~4n5eMLdL!`ia2SFfKUIs{02j|}U`rytC z%bk2v_=_S7>}m;wGGC5hkCwpwx^wMg8d8ZFrLnZ4tWZvMypK~$xQBOVh(|TzNLN?c&(+i2%{MgLE;4~`lv<#csvx-OS}1F<66)Jl zh>Whv!lmaYQwmd}8P$=&+!jAy*Dg=*pdNSM#2ydtlpas7)Gja2w9bzOh&cZp5C!J& z-XTF=3dqz@ddt((ep6^*{IbN@>RFXJY@rciH{N2w?5VTyX(&TPmE_x}Wn~fy;*zLU zK~cskPM~)mD=4~`6PVoV=$F#%?33E{vHaU1N?Z^~l9vRsHJ|L|>uPKl>+8R$ zFfv`KH?x>+w}cP%7&F>iEWN90V3B#H$kgOKY<_qqvBE2f+Qf-rbyB0;d&x23J=AD$ z?jM@k#R~d(fJiWhXn|8eH0Y3`@OVF0OMa(NTlHm`uGU(uf!;#9x!%N}p>6L|1A0T3 zxo2^+O+-d5JSCy^kKX2K%DgVtdh)zVTV<(5Uu9~*SbcCx7uq(VPpy1v?2+4T5uVTrNe-#E&2_J| zD`OPf*Wn6CE$Bk07IblNGpa1TiNMQlB&BCHe=I;0=n!-76c7tKWGag87HCLpm1{_^ zHfYMs^y*0rP3fw3E*RL<%^8vlCym@vModG)`z;c^JFT)gEszp?1H2kpkFSR}I@H6P z1M6(tQftxm`PG;bUhT&ML|^zP5DPjaJO+0PDGR?YRukE%Q5Bu*R2LqaP?79f(9&sG z(}R_*7?QIWja_1Ajf4FsjpLn$O)|;-=7p$kn{s#;rV`%GtU!Qse!HH?GP|CvQbc=E z*~bFJfDZA3CxH}+TYLE`0$UYI_m)}|Z;y=1-0PZGRA^jRH!j=MM&)kkG7_H|cm&Vu zg}P7b$FoKZGw=h(c}Q^Q4x-Pl7|}-pe>)V~_4^he2cq+l0~vWA3lMklpFkq$kRf&L zO|jzjwK};g69W?GyXT}HHm)nEmT#+A@?L9TQ#N%tVc_lvuX(Ld?xc1cc~mzQJ!Fs# zA2j7b2OxRSK{O9KNan$wI^^08`R3RTg=c>(K*Gguf#mxq_Ia}BH!Ee&&UQUI+B@^$ zYSWtJ?>Z7MWHfFc38O zH#yyUl$&ld>XG)b07;j=0aAteUYE!oUuqWlrGN6~vF2wFZ&z$f%H-|K>!kt8u*f%x zMBhzCj?*(m589$q0Cr9}909I7fJ|z}TTSRDT1*%xnon3HnNL8$-zX4Lg83vf{$l}B zFMkc>KKymPM&!GvqbI*@S-o?*a$7_&e_u*I4Ujj81Y}{pJFSfu(uO|AAeb%;o z;m4|1_s$pWKN3y@Borcm$ND~C4Z>+p!k)G*Nx^PPvEdui&em(P?q;hBUdCW;#&B8H zSASX4Pj5-bUvJ64Uw6p_#L7>18TQe<%e(x=eCx?W9SgsIQT_7zuLbXfE~Nn?!V$m& zIUnGGwj&^7P6b5am_0G9?Q3zeJaeQqFE4 zF_;SQ*j5>kge&b!Bb4@K5sLc?h$s7sb_)9{AZi~C;Bn%gfalc{fVKSaEl#2+x-|T<{A!TGR%_F@D|3GjYu-C~3_qDrwC+5hOEd%_cu- z9a5gSY+Ic;XIGmzfv!&&!8CmopB(EeF02ebO21lOBp1fVYMT;H?J-+`$@z zBUpoAz&&3Pu|Y3T)TnhBH-62=1MH81$ zq)|*m;vlZ+qX0a?ap8515AXoTg{!nQ;G?Jp_=9y2e_bfx3)Ub!ELgh^Q1@4Kbl?j- zIda{Oo$w6mma=LSkhW?Q36hky3dv4;1}#ZjL{z3up{i3yFm);Y_=c1&LVfDT0C=7F z7VrigJV6I{DJj5TQ3VJF>mb2k4I&7vLHJqF_uL__+Z?3-CWR2Oj$tM|gS(`zK>RXR zAd#6XkmSr27%yWPUY;?Hs!AWh)~5Fp>e9fC;pxrf+Ki6@@CF@xKnE|-!AnXK2z;Up zgn@OCaBwX~m=PKXHmAPzwQ+jwg7Dp9;lekF^!QbzQ`!p5Cu>$zHO}%bG=& zXN_X2Gy4fOnH}Vs%tlIeRxPzU`(yaGgSYT4z(+~~2zjCiM1pmYXdMe6$_NRBo0H!L z+c>=ThI?%~VnWtv)VL)qH+9a=GkY2ynmcKim^+Ee%^A0^$mzpZWp|LPvm2>ZIn}hv z-13hE@Bwr1y>=Y%0Ui7#9|K`eo&eEc4I)n042Uy=@5h)E-bC22w}RmAYo6$!MMrY< zG?kq)hIPvxLI?5&(6M>_n9RIBY-wH_u_CX5TEVNLSLBs4D)I^$<@q1O2M51@IRr?G z0})^iB2HZsNCfBqNyf0fL<{W8I2-y#B-~{=5bZbPNs1VCU?ufYoU=RdKKX69@WK{+ za$yrOzp#!{R#-tVD=21`73Q(Z3UfXZz!%Kny@M~9L(uvK=PTzhX+pcBTwfA-H zy`u;!z1IK`>P;1Y*RmKos z$#;qEPUmGc#|ug;!i07Cfs(c?QFLd9D5En?T-uo;uIWrhB#CP~62-M=e}@0VAx-%d zT$=c>_Bi(JV+D>$#*g58G4kxoV)uW_1)~7BrOw^Vr z3GIpvOzMu76m&-gRdq*7s=A^i)#sulHJv{L#GrMEL+cQW)*(Y#1`2f*zf~A(e6Att zzpXbn9&e_Q#@gwW+ig_#)p{HM?h41~rXqGmMYel!Zi=8bDMr{95f<1T6cXJV7@E~9 z4y))145{pq1Xpzj1yy(b4FB!09^zyUL8i({DAtposWMTWsx#Go-9$2YdX|j4-%TOk z?6PpV*h2R^Tg!;5EoY?{6>^Hwv-ma9$=7L1o8X-%OP9k1eEHZ`CMbH^r6v2{bd_b>+yNA_MOWFi)(#mtnLm=-==1} zi1J#;)SPnHg2X~@RYU;Noqqk4b3iIAv(49QhaJUt-}ji>{C+fobn?p zT`NV!oF?}?k4{!jK(}*VT(?txUKcCB=A2tbO(!?G`rOX|acCV9kwdcVekf2oI#X?M z;$x$s%tVKg?2}6-(szapRIZH}5W8=fI5u1{7nJnTLNmK;l49HKb0v+ALW%mpPjv=ICR?#bpZ8)9 z-5tX19eiM*bm4&!q4f@)Q88rdnKNJ!oX}582<@Y1`*hisvd=PW8Exz)+je2I&Dp3X zo6hV;>#q8GW=l)GLrrtz&j1O?A^G4s$U3zP$~E?WY}DKL>YUE*2Ln1=Zr;~E)c@2- z@!T^LeElPWUEw_fKV^g{3BP3)hx!kh+^f_g#%1eD`eo-D%PZbBv}+O7wCkBw)EhOG z)>k?zY`V`@{S1(V98!@(-sx>KHCo#z+cdX6zNETg=&r)n{%4ve&P`x78eic^W#cBy z%%>*&=*Px^0b|B7{QD+p4x_|;`Utg{cH5zZHsVoYIU*^xyc1t!btkXTYP6w%Hr!qK zGeGj6ze4)KwNP?q(^RA8#_{t?t8a}QTY34Z!tSm&8mG}c5dDgGhSZ$5hR$)X4ZI{1 zSc%}7L5$0oQHtGtH2nbG)1i#o21DClH_b#S@I++K`8i}At2n9vD-&ZAP_i1H!r*|qoHgD7ds1uq&W-oO^i6|c; zj2lOojFTda$EgUL2>c5N1ScH-!Yk}&fP&5QCtJ5-ZroUi>3OyJmzL=xtE=J6?mU!1 zB%ll;1Z5CHI8EiD9)!~uc^8{$MR&>vWiQe@HDAJ{rl0X!U4Psg1Ch}ie1PE_BEllT z;Ekoo;H|v~`_|e2XZTRP1T%PH0jBTS5=`sVu0>UFbZst3?MZ-RCqv+vsxKTj;DR*C z5oD~a;gmi3>lr8F7X?@2Pf8rzv?|wdTGJgnrQ@zYh2`r{8S(X|O!&G}Bm{-8Gevj* z83tPCV0y03!!&&S2OpZ&-wpqsBKA2la2HpQ5*CE~feKIhe99i|1s+x<&Eu z=jtH%YpXYGJ;;U~C+%UkA`SLwlHi~|9=RBRl!+clo9cipNefP!X@I=BIw)GGfeKj_ z)X6HKNmd3OvJ&Vc41Wp`u3Zei+t-2M*gkMqlm~AOEfDG%gAd*kJkTRxTe^R9Wcq!! zb_O$-jn@ced?61V! zWnTLs|KH)i*69C@by%|)gxlAGH~M|>P>=(mx+eJR;lU3_1#hAQ@X$XEEQav2Jv(^H z(ktqNML^tpvxtQEq@=|6X4#11#P{Yk3GXdh65f-$<6l|!#y_*Z5I<(uA2-Up8hhJe zAnr$iA9CYwn=A}r~h zd1CTA^Q`1|7R89V-V%9wp$%BVws^54!^ldrL^r1ZO7PW=%eLJlJ2AVdx#Iavr*Re=bUgG8bn zBplsI2}09+gs2C>XNsp;ZjtY8y%Q&CL20iku^AJTjEq;*;*6J;wdpVDo#~J4deTN6 zE~fqMd^z=+OMlu$*Z%Y#_N9y;@sC3Qa_~bAfoEhO0_7kvI=ZMYMnHLx6+~D#%}6Zy zQ$F^hcRbhd2`A5lahrg&XI7D!Ppne19$V*UJ+i6F9J6cByvyv%7;?Upan1Eo#zpqU z%x=!btPakF>>mLEXdT4c*MR@Ay%2N;_0?1rAzoJ-5{-;eH;4K)&Vo4=Vac5gv=zSc zW`#WC@Z-iD{nGB)g=UZ1CFYFS=j7aGl;;dHTXKdRFJxbFy^!69KKC5AFQ<*yo7?;o zfOyS95bsz6BIFQy`UJ$GyFtminviN_1gWMLpOP)?-o;zGzKXCFJQF)h9(wbl?{Iul zhnyrigAOtI*BsLGuR0awUvaL_?`L)A_qg@sckp`hTiko{>-pykYJP%$90HL;0CEUF zeH;={-#krM9Wrs)nJiP{#|#Vl>r_jp=kd1uu?Q#s(Li4KO@Sc!3P+T4!6mG)hm}~= z?UGyE#jY&sbZaj{BN2)k`Q1ge9^FM1o?XRdKLG@yb&wzjF>;7HEd|MHry)~M1#)n@ zpYn(%leuK-%WO-=<1|~hdx_58L*bmzD*>KKJzm0`4sK9!t6NNIGbf|0ky~8Wz-uh6 zMOj>>=eg2SuXCkEg3hu6!MV~O@jniM$RXy;QAkyjg={?~$j56<6_Siz7g5Zf7g||A z%(Zp8o#x;*5X%nk3*{$vh`h5Ky#k7B_~8|m{G`fqkG#q<&+3X2ua1gB?~aN*M7B>y zMb=LM60{CMXdV9J5O?MXq^qBVJbi_kBD}`?Qj-3wGAiLo8J%*s*oJX4o8i%)>=M)! z&5Lgi_R6aA7Zw+L2UQjbVyp86nKe1yW!2d}t<@R6?bT_*_UaU2dsXsJ@Q*|Aj@1x! zbPpt)ISiTV(omo;_qo(qWwOFtd%TKbI96q4I#Oq#^eRdX5>~~D zf*PVkag7n8oW`(#s>aZOrp8cFb7PpOr5;iDBmQv+K@MR@cR{k;0mxH72Bp}OAF2s* zFYAa(kDJWZMq5Zm*Xu2;`zq+%wjyRgZ4N8CIE|a08ShaT7vWtM8YF5A42)A(cIvi)ZD;||1l7{ z<`)Rxxe5|cXD(NM`?m_Mo$nj71)jPG7 z9un1Pmmscj$o43AE^#j5RNEE%*4dUs)Y+6~)Y_C)R@;`gS26S2s~xi1YJLQSt^Nff zkVCQ*>Ojc<^|?lS%j;I{&5!#u*WbRaxcTa+>hW{KI-2!^hUUdr2#y&S%)MgIQ-gv! zt>Of2HW@BWj6%D5mvXCmL4{>wSh;0WS{bdmyp+~*uEeJ1T&Zn&XW5T{aO4n$9MYvW zK&irpkBwUE#=BHkjowiBZQz0G#@jN7zu*}e#ljl*tg#z+O3b8 zYTrZ8rJZ*yr1W?el6yl6EH0$xTU;#7Gw(l_OYQ2-wQTFj{}B+mdI7}jTnRZ+t7obd zR=sIcUiR>c%%bb}PcG{lSJ`^@xvo_0Q$w|)$9R*J2S&ExcX3?*5o3SuFd>35L`5=4XjiRXJ4!Ase7$`s$@c6C;f%KS@gIbBjA}H&;6;spTlEpDDAOP4Cyg3 zk?@$FWb)V*;hRW!5*|-@nifZRS{h4u))8a+_|gx7*fsMZedm&Il}8s%bV$#?b$j`o zi_iBjIs5L^ruvU6hsr)`DP+FaG>CbpNeO(X!StNe{j0bF+CK=2HIx^!Y0=4PW*yDuJWxGT`{`7&tC1f#WJ( za2)Fj(nNcZwWOim0qLu}qcQ4+7=BiB!+z4@=uPW$b*FJWooQ2^_Ou00Yubvd^@+jN z{N&2f_~iaWhN>`_OI>p@X9wqDYQL?TTMXNmq{H6z(XfAaARIXE1qT&e;IOtG97TnM z<3u8yq~PI{70MrM^gzi@2hAgGH9c`L65A6at8%4Cd&f}Vev!Gou7lLyMe(JKEYs8VBx$7Shh$6 zt5@=2-JeddaW@_QI${RfWQ<{#JQnt<=)eJW4LG8u22$w2M;aZt$m%J;8GU(Bz{-I# z_6(?DPk|;@7IX|`zyK=)xE}(XRlkAD=GEYMXcsU~OM{ED60p&o7#BlRV4Bjw+R_Qg zOdb$8-rsOOeqRlPL|+YJL|?IK=xaWrO!U>TUi8hd)Bl^%CI4xYtN!nZ1AY_cH~gMc zhJ=qSZwntfC3uQv z@Z!KPMv;;)MkzsGjPelWk}tSs$qep%;1r=h@U7WZ@eA^G@ng%I0r%;{0k>`Lh;RJ> za7PYq$id~{PT!|_pSeyjM(0m+^`!7x^6cTJR87A4jkm* ziX8YSkAg4CK}70mC=W6KUz|DcNw(l(>H5{)!Rs@XE1Dt+gFg^L!rl=Q!X^n>VQ-1W zVQ)xvp|8z4LMJRPhdiYXgxt5j89ZV)9DKw6cE}aRaA-gCR@iJHhsD4}4s7J$C4B@$ z^0FXNR{@EhK8SHd@FiN$xKmlvEPIdlwru}5R6)puxg`9hSxn>$^R&q476lQ{$W;+f zscqp;X_vwtSPzEXw);DDkZ~*gGIJ=R*KsKFyyMMT0T1NhzIi2Zkb}>OLm*K&38Csr z5T>UCp*WMTfkfJrFU4V!XUm&#VhP7>Jc6IlM3Iju5z!B+$*#UEof9zREQlCjh~ux=MJD#!rzBlu_-@koughe|j<(uRNYh)E>(r;awj&cc=@MAM78@xWZhfYRPvP+JE!w8PJ*^jc5rO~Kd#irC!@eq zoSVas%FpshEy(aJDoFRNFG%z1DM%Ld6eJ0H@)Q1{H!nf(eZXhsBJf5IqJ5hoT6#OA zD(-Z|jsyN2yTyd07_kSpg^f_PnBfxj%!hamZ`~HMj>8+5ixa(`a=Kgmj z*dybW#>XF46J>_0j5V(oo12}_r88U88T^_=R$y^7CnhU|pOzTllNT)%mxTL9m4*7I zmjw$;%Yyt`%Ot{1#JSQSVOQ~w_{YHqt%C&h)#9bMK$hZ;&qbPh-c(@sKdmu7bicvm z*v(oa^-JYu#Lgm1`-U7l_wqESfV@O@RBALYH9EvAFG%cHDiVcO`iYV%eMJS8J^>At z-lDS=zM_tDp?_zYpZ~ei*#IG02Vdk6f;v)3(wiYiaqD!6=8jj@2D=_N811=(IsgNW zhN`_aL_%vB)vl({hFg^5;Gdc95|NO|Ne+$X=lX?umw8CU)jUZ|4L2ye+AX*W(O$)s zoUP=GJIX!8owEXd$U(SyIfU(74=K`{AYXCwhjPs=<8|0A4_mQYZk7FP7!A- zTtYi4Tthm_XX76SKjaX(Z!M%tum4u0xM8wdbK}!yo%MISwAKywsvhh+r>E4`W{j(D zG@}>R(Aen}HombX%+TO`=LFv@c9vTzub7$SQ(>1BR%x4@T49?~R&JAew$wi9Tq!da z5j{KLzw%e`M-DOjRzsHbnlEJvYbNS7SC4h5Eg!xjyXMMe<$dS+bmSYlj0{Ur9+cfo zW+l{H3BsyvgZ;}Haqh)V8IJkv0-IbxiB(=miB*0|k!3+ip=DuvfpuO_zD>sYLff?2 zfdJ$nLJkRNmQ~(~6;o9T%b&L@|94k$grB?M0D(q>!rg^&Z5>`L2*O-!Wj^q^C zPWBWxTLyYI*hI5x>{D&3SlP5n&pb+1P%fo9DTiEBoK3E6&7xLc%Cs!GkZoBsJ0M05 zL7SIC>b|9DM(gjB^>T|Jb)Q^t>-N@p{iDa1cZ|sIsJo>mQ#7cnlYZ66Jf`1-Dd{7* zd!M%uxt^m&*q)^)P}&_*&D-76%+3a;nRO(jkU9&KiRYS=NawC4S~Oivwy3|7G8-US ziSnq;=uXbw#WR)17QSego_A+(E#}Joy$iY?onF>Frna@>f!2wvQEiR*+xn&c2iYCx>XwYX@L>-kZ}gN>2KH?BtzF5QeY?Y$W_ z8xXu=0qO?*3I)3sysO_gckJR?%+-f0G2KrOEo>f_TT?ZzzAOKkwoLL9ZO!mUIt0Im zy4KwL`Yz17242)roQOC|3NpEC9b$Zs9gM#(48lK*lHkWO196Wk#kj}k1910m1>o-7 z7S9HRB8Sus^S+jEo-^LL6mxCxSIqhG&2w8`OZ{H`R$)`YTa|+;uT|wDC)9LAuhfX{ zFV(FbUTUytFSOi=FZ6tHFY*2cugD_(344*=Yo5REn*cxEH?cyUw>iE#lQllt?=J~- zCI@E)BA3sD++~>I#`&15eG4$>Z~uyEncTggdg{cAg6Y#+Q>NvPMo!Bs223mHc}yu1 zou(A&R#VCjq$yQ4ZVL4dK4Lv}KM=fhK2W{1J}|vBKXN@aKl*uSOhxcDrc&M2r%QS2 z(>1dKDN8WJ<#RAs+UH@qugu3ZPp!aIec7`p|J%_uDR5$EB%G8A02xIOkkxgDQzmpc zO)-NrHu!If4hA!7&U#;TT(v(NvNb=OvNb+a+3H{H*=k?duBu-IE-EvDEajOvXXTmX zS-Dey!CY;agX!*_i)k8~k12=MbMs)wl4RJwE&>ki6u}WG4>&6C1jjV#AZ2Ie`17}L*uKsmcI|M7y@wrP-zh6Npo}^YT6j2$)raGFZIB^ofE-Z` zl+08>&0GnzEffH2ArE*9IUre_0jl{aur-$jXERx}QJRh377V8KA_h}2gu!ILnum#p z-{ytDswG0$u$l*d{^bB$_FBUBW2UgBIcmm{=q(E8e7^o{9 z1#P7xps#cYa0ugB0sGZU!4mDe&Cvg=%}*W%D}_@)S62lZn(Jk5N&;g`I$#}`pv_}} zhMy~_hp|B|5s`%`M%17`YeQUg1&tvW&>435j2&Tpz};oNF@C^$VLImg#O#UFZ2Ujr z!1(WVFh}b^kv;@AXuoNzrUG<5EKp2HU`nNfA%h9J92RH_*`OZk25Je2EH_XuM%1{0 zMmrlcFR(%D2K%etuoL%o zw}YxO*z4#49dC+aH7hW-V*=LI1+=|6pc%vkjW`}?Wbi<N=Kfi~!Tl4qllRH+GVdd9nERS= zhx?p#k27ZRkTXho%=w%8gf|=iIM{4j1yr=pVxs;5`_ysZs>lOJTMJl*c(5l@fM#V6 zL`T;doICF`*56}VKiqQ)o9Ow`AlvhUVX?=1qgs!5xOV=e@kRbylVSI9(y03*@&n!| z^%3uH+GGBJyJOe{u5zN2Z(QH3a87?19 zIqvWAK3%Il?Ri`NTcpXWH~Z;wahdmeXa4?Tt~$2_mm z9|^7|go4tR5+mYSib?PwqD$9aUO9cc5`d@h_#A#jwRy_Z91h=;db* zgk5&`{bmE4k%RrlmB84&8QhQT1mUTJ5TGm#Vl9PlBCPhbkFoJPo;h{G+1lZ`tuyB_ z)m`w=!cTb5Jk0;DMS|ZQa<<k|G=yCEE;KM?lY-uLUVyC>?jzZ=lX zm>pmt2PSfG+_ee3j%KtbWTvjXr zhYc&hW#>llJ-iiyWcNV0(vfdbnlc|Fu}W{k@H)>WB$F|rrRAsx(`m?s&A-O*^zXOv z54vC-7Tjx-7;@e=JEYsTG~}FJV@RicPe=!2IHcL}c1XR`aA>vjaA-N}R@iKSD{^o| z4sJU)fdApYAXIiIL@OQm5~q3mT|D;8%UHbn<4BUh-C#@ezy0hPSNSYnpDWM5%fTo3 zEJG6B#)yt+VWvhjF$*FZ9jYT596KWFoUTVyvxXwdU4|lyU56qI*f(bd*vP>NIk@jw z2jatjLZs|=h*#P>ouql>b&9^svt+#Dg9M`X?I;>yAjsDCg3yU`j_)RHVS9$uJNrl0 zIEBSjIVZ$aI%mg}v&v%1T$*D_UHW5+*n=_oZi6v7oWa;k&S1=J0DJioaNe*SJa(*w zpu-y>S{BWUQQGw(P4mFZ41KA`nYdGT(}^lK<0;1d5jNJ{5(jpxud8pJhkI}Z$0xdk zEr~C5jZVmCrzPaM6(;1k)g@$edJ;0Z0}1K8frL~*EO33X)>dc3m@W;0Ebj_mZcw48S9A`~KQV(F zhAIGwDXF~Nlw|kHlqC0#lmz~jlz9G?M$p-;y^b^wvZp4;^mW+;2}wi z^@vH2_Q*(&@+eJ@@N7vB_v}v(`-jVEp`MphXX76Sm-WlRZ^ufAJhTRqW!6t;D{gvK zpuXi%sqXeW<$C+B7a7U*<&v;x(`l6YL_4RlXh)B{FjsL}AU7(`&nqdy+dm^j5S|qz zNX`xv6l9CN>$3yAFJy_lFJ=V@E@cJ?`qO6vxM&?**DphzxfKw7Xf>qCto@Luu>M7f z`o@QqI-7>8HTGO7GdSH{XsXweO|_^_vvDX&bl_*kutbSrZV?fI{G>pCpG-erNv@A? zT%NZuJI_m4mFMZ#mFp?&%N6)u$oBTRm^B-~Lk?`@Al|+l;ts8ZjFYS070R!ER-wA) zUY*vO8+B^CFIMTF>L?}X)EAJ=%ChN<+%$V$YLb(GOpI%IP&hZySK^uF9^jwP5k(ZT z1JVjy#ia!-aeKZ?Ku^A#s5h7Ee<6D|zJsOg$Vs=4^UAf3mE_Um z67%Tsg*jFUP1*E>i&@q&7qV<4W(PdbItbSP29evAK>GeAABv?GJ*z$W>uAUE`2#)2 zmiBZjY-#Dxl&WeqP%UUA;L~c&tzs)_tl&~>FJX~=AUDr3nwjOALeKEXpk)MRQZwT- zs9E{xl&AHTn z__vOWXa20~RXAF5J4*DV|c|o@7xHkYrvO zmuOy=mta;_A8%HEDb~FBdK@`-AYnE@unct<*DZv^EepOB?3_1VyK~N+o@JP;18Xte zgZmdZU6Jk`hUUn+**!l z9ohS9^@#MEf?rFTkZOVab@>>a2$#tj$0gh(dEsu-y^V%kybW*p z2n>e9y$o)rdl-yV@UeHg-1SHA^00%CJZ1v|m(GWjrI_2L^Dx(%=VN;M=3!d@{uNXE zZ2P>D7gE1xzc{rqVO(Kf=yN4m;WHHt?o(Aft8>JdbnL8P9dmc~QsGetE%J=f#-QY(UiFIS+C%n1PzPnBLC0n6|6)G1ae@ zVv67H`X%e#krfH=q_>2=J9WtK-I+7INjXi2Nd+A3t&+LvTNS$Dq^7;@B-TOe9l=TC zJ=ID5gM*{$N4|r~$3UjaR2)NjI?rBty4FtV(`8$wPglOnL@EX|Sc<{)wame^Uzm%j zK^a8h$JKK(Kkrx+|K-5y(3zv#{AP|H;mt^&c9=P-ZaE{1C4N0^V)#|wT=$y_P4klU@q2SFlW2xU}^_Y2jcEL zOd7137Ylza35H#3ePQ1=E*w1M0EcBP;iwW3q%;jdS|9ZvaOm!Zi3+HgDuNbC4zQ%t zU_v?tWReWn5v9SEcpSV;r9eVBiY~0r#-&OOrn4P`sq04_2$VsjPAtGg!Ls=hShvI* zHm~Nu_ALz9wT}vWq)cJ|DFZmDs0~L@8U47X63FPtgPiUeP}Y+L4gHg#r+)%)`p1E! ze+(@3j{-yQFtGIwfw%4<@cAz1nlYG$UJR!AItG)5GKk2@IdcMF(R@!>y4V#~udsvl z8_DqJb`#jLPan1))r4IqRA8U1JRCf88f8SXAdL=0PbnS;1(Z9eqTE4C=`djb$r}!U znbLl+RNM>9S!wFRU`nrGFd4Tnn5cUgjA#-whYy$q&ahySH7xqW41QmQgB2TeV9gda z*sxOpHt#(JTMwLsorjLY-Xq80;L)RKj_P4JaqJ+-O6>=EslA{gwFfk%c7dMMPB4<% z4#Zgjv$e|sw`Cn@?%#sGWe?y_9|Js^w{N7U0eS>O(6BTGWk(B8;8Q^^fCgtG5y^-g z8pxH>K)!|s3T;$S=%a$-AQhDEQ9$J>WkzG1Jf-uJJgGNfF=6n=;)UT`^X~!XYnK7; zuXUh>91M=_0Tbk4qH+fC$N_6&0Gd=&P;nrGqC5ILe@l>yKqOg$e6}ShlvsjdHKK(E zNheOl*< z)g<=0)k~w7mQRf*EFYM>vARomgT@>EJ3v|U2bgSL3kG{OgZZHyKtFj9Y*5Tb*Hi|I zp)Q!35x|g60Uc*+(DbzXsv)+Y(TKGFqM2;}Su4lxlXi*Sw05oClun!NN4-AV5Bftk z@34=oUm1;CKQ(?ue?XY9zC(O%dyDjafV%n*Fx|Wc@MzCVJFpGxrT2paI{Rm8D1bFq z8z`p6U}{O8F?6u`q{m}S>G&}}>Vz@h>n1SY>18q}^$Qtqu~m#W2F>=b4bR(87!BIJ z#EscKHF;rs-}II3ZPJAO4f6@cHH+8G?*TOAKtv9vXwPeNU<){-b9c7pmQ)*baU zzS)=`GhdQ zd}R87ai8>(am)Or!*z>Sj{TGg=L^(V-vz8zEd|nlGO_LJ!D0Vq;GEb6e7VEGN8Nvp zuF5AT)IYE_C%&fGSiK;!n9qzoouA+XTpk%mxQv-3u^tezS@%s#obM6qobHl3okq>B zIo&qD?=)ob!s#mICF=t9rE9n43wEdF_;&$1axg~@mRr|?%f3zEaeO;?%k2jr)f1lu zItuT3Mp_fjMC0eSR@6sSN5*|K9&3~+WRDO-*u$ha_ARpv_K;a2`=)s{d(fhdeVu%X zeT_28zD#?;?y-E%>7+m7wpu^qHO&H8BL|C3tHEZ=8sMOFR-fZrKy+r$*8tTcA4S@y zUi%oTK6f|8j=9jxMi~s-TUKn=AVt6#Ad7id$dT@sDM{{^C^_yIsip3Hv$8Fu zo2!B*+lPW$`!VlI#+Y{*^Px}iEPyR?pl)0R&VQ`{!QTJe!$7lQPH*`bp}cz{QtR-O zaIDO|U=!tA{uG02o;K7AY)8i~M>k%(otL25Ho&*hHbPi$n<%Wa%NEwymkF!wn}k)2 z3&INKs9%Z0eZNA-`~G=O_x*Eb0ql?i4LP{}wH$naox<7jC_?yLYvNuYUzID4>iPBQ9}B6Bj!aiVGcU#RX1X z;(VteagOt-IFmIhPInmue|L1wD&D;eB9E?|N|0UiDphgAqYU-G z?qq21985Nnxfn~*>(*%{#pZYAMy zZcX8_oJ--+oLk{hh)B+@un5j@==T5yS_k`e%TPb*cL>_O6k?7n|Bx)Ray(Oh&4V1( z^}{(D+pne@O7|oY)!SmICbeNU^wL14OP(LgD_y`1NaTBk#_|Fpqqq^#5!~eHFkV4) zD6cj;gx3=t#JhpG85P7Eij?q%BEAPOk%Pn9KTs#;HwfAFJH#JeI+-rL>{-s475562 zR^BX7+jc1%d;DCQscKUq1z#C$ZCMz`aL$xCdnEd^MN!`Tuuu=*7zsZ(PRx&s7xA;> z{XHt>XiegJUqxT9CGfqj$9s5PkNY0rfO0vPwZDTOnne-0bJ6FN1B+he9$WOV zM0(M!$`ea3mn(1XD%L&RoR3$m%rY}5OtU0sB-%0)Vwl|UaA%=7h#e{jaF65q`6jc4 zA*n9H#8hX$f>bBJh7?Et{uC$S)g%|+Yl*Ht0}0;)9Fc?Dn%^K`(?W>Z@#|E^zF(gg z9{S~O<>C2*wNk%bs8-x`wp{yQU9pjTX+BXeC!1oPl5TAuoow$G6zAyU8|f0v3+2Q* zNxV|*0|V1+17p){CAq0KLA5E?L4C?XiTH1tV+PaETnF7y5X9+8o z9c3HEPqc~@rO+Z{l4()dNz~}-1Zs3|JT+<{-ZE?;-YVq#fb+7&;I(=ogl<>>$$!mz zoxgR?{i=s&gWx$xto2j(`iQHlzAhP#t)s zRy@ZdTcK5fV~|B2H_R;0FPxMg8AdF~3?UX&2AdXk2bmV$kq~o6f=L;-L%s*NE=6}p zSI&p{)pOpX^Z%jBUocl&7hrmN{=l?e*fqcU!inXDy$V~?&ubow>(o~YZO38#TL>2X zMl*ZoIx^e3#>&g0%HH3!iY+Eo`39I&hl@v*0w zGIi-Pq1$v4oh;6hxrBCWPkcMe3wPGr%cvvN)2JhrZ`fJtZg8%HXK-$qYta6TXV~m!F1_KJtpWV~)@ zsk>g+#o1mnppdWN>`X2bosBMAx*A+@a>ZWtbkV;S%+eo7cGkOI;;1{=?w~t(i>Y({ zmBau2M)ZZNb^9lrz6VGa&KXahiy17!U@p|n!E~HMw*pWzqGWg>Cg;(%c`1*M{2u*C zW`kr*ZjbjvMQQc}6&1UC>iXonT11n(`ZUA4#@70G$+o)p7`D3ic{Vx^0_Zvq_6ezV;xi zypfnIzfCYxp3EmHPu3Ha-d!|Re1DhlJw{S6n9C&?OjjcY({vt#DMQVOtlRT3iSJg= zjhx!LC~$i3D#7%jzucye?YEykAx)V&sX&-MrGcH6)6@Q>Y^eT86R-RkYohptU?Ts8 zW+FG^V0?OpkC*)_#>sw*Gm`n1V~7$F!;{c~8Vdi;HI#~+ufceU&Th`O6OW@1?+@V|rkgxGQdMi*X13iPPJ`5%or4k9ZF__S|^XK@% z?+f^_da*NXTww!$ZM1;xJB(rXetp=7Qi(&-%5V&|BBW6&aavvm6wx+GP4O7$Djorx z;vq0Y6{u43qmF?dTcQpl6VO6@y7c$xO&y4952zW)2sAS>Oo2 zEw+Lc%SfaRwcqD@pAGjpKVz ztgs7=rFMY1)Ha}_>xqtNjJw-+sp~|k5PAm5=!Q%vN@jc>VlZy+F>@Ix8KJ@4#isD< z9|o{wr6w#}rwpq$o`JQSPr|0HQm|#)5wtc3VfT*xuz%-XII?pW9N)PeWOr=^g(&AJyMW%hm7uz91DrXy1(c3#19chnqbQ@L)s+V&6ZFVwI&jKK zA7nfX;A8+I5|Ly8GFb*7TZE{@!l_0qoIZ!jLzng8%q@M8d!z^Quk}D-O82w!C*6;# zUv%DTeARjL9Y7yBs38Z1{Yd@LHqesZ2RbNl($Oy`T?6WxCY43UEd za!^Bwgf3bO-0|IDEPoVmsD+O;QUfhBJy5ejf4{3SD0!QJq686b@=YnlS6jtHGue8HG(*;mS{Sqo@x3~ zqk!;1qk`~3v(eWmss=#JxG>V3dJ)t@$gVDQOg#ORah?e73Ml+9n{4saAlu@JVX^sRqZ;!^M(t)}xJ#r5_z}`w<8k5${)71qlaJ)9rc;y)#3@=I z>E8hpzF_{?y2t#TO(%KMu9Y%r z-$;GSXrTQ&V2T`YC>62%b0s+MK&^yBYe8^w^Cxfloo~I=50AU+$v$#5Q5|Jc3~tfw z%&wDN=vT}hE-tu=W<|gUj~AMtHAHXh7TgSt*`u5_dfR3mAdO`EO(1z zp?%H4nt0L1k$#@WVV=QeyMcwb1s3Yr%H_GnCYstzQAmO@9Dy`%)0@U;ZWV_?oxDXEr?#R@weQqJ7}D z2!C?G%S_`U$I7J3iDA`d=i<<0?cq{S_hZ-6L)@zAac))C>24L)g>L0GHEyLgoo*$z zgPbC}C!Boy=bRk-=iE%j_W|=&e*k&ia&X?X6g;<~9oW7BD)BFfqfD`-#&+*%P8mPFk1Op z%**^t<^z73!xN7bM3UnZ{`dID!4f6ut{Z;?Z*=-A+56jvh@*du$H=UF7^kpyI96lJ zwQz%@y+Ni*XZg#Ko1J@5JZlI0!v69YO@@4zYqX#|%M=W3eFFu|bgN z)F()Ay5k+^H0B-aJSK>Ce)L_y0ws0UsL99KxCDe-7eUCL#goy8e|wrBz4UIf+=`ot zYMc9Gu!p5)yJoPt{1g{~cOpyd8}A$`jAbPYV_5mZC|0#F zlGWuG?sCg7%;kYF6cOU`z&FI@;lBc8$Z0;KtgJ{dbnebf;U&kL@^e9DY@kqMN z?*l2SoB9&;51x%Sk*g0g*D04+5)1t7Y%&FotRz00AIstUMzV$eVXh$op|0@(!R)L6 z3A5l_-6h^886gj>@Rf*6hwB6O9`hwUr$XqY=cF^tR4599Fb!q^^th~ZE-{_S9{`$!Pi{f^|{02)dQSu4>_c-=2(!uI_4 z@qf*IlCf>>NWs=Q*9wm=I-jqwrZq=vS51bIbV&+PEjxi?lpI5+#Dv?S5{3iEKY-=q zDdb4pyanMbL13hlAU4X;J3Er;T@}gj?vAkc`8(V}FpPRVw?n=MSg!aT*ee%;|Jnr* zxpD5Bx@`|p4ia^S7_ z6;1J(_bq1KoLA|qF?S158=!n0rl<12FKrd4SJafMZ7nX+KboCyteBKzrWch#B?YC> zZG;JSF77c7o=y?00P9e0C@sV{f)W}YK@Lp~BZrlST7;bqF%KUKwg|Z$Opzc0{~fUX zV+m?zEP&uu|BJ5kj%qUN+Wt+IF4BANy%Qh-5=shzkdTnjd+$|xM~Z+7QWOvY0TB>; z!#?WR$Nt!R?}AuRINzT4UF%!(FuZ^K7HiG8+;`47xN=?R+WY38>y*Ww4v1nm#)Pp8 zyJWEwdyED5@4-oL-{qk(H0fi}GtMHcALY@ShQhct15q)>eR1j8-N^-sUFnsP9eH)3 z?G+6n?ad7#9X)lyo#U&5JCD`|b=|BEYJXT0yzXIb@Y;u~mI9?ph@$f40%(mi_O4qD zyE!a`U78fcPVJY(rVbeh>^fvGIeNfdb@M)oY3FW+Q_BtxdDTP^yL4N4WbR0Ga>|zY z+~|Rnl90_gl|h@!s(725D!KjL72JVsWq|`nN;v~JN;sRJmj-q}E$41r8k8lv4An~T z&#o86Zf_RA{u&d&&g_9iz+p%v9M|R>J7Fz0a15`s;fR;v+9@B0`U6al^1U4Y{9Qqz zX*FQzp4hN5U}7|%H8GXP+I}sEIq^D|HS#*2HT-(z zQlJ76L9|)~yT1Vv2}69?*&Te?v4a@4=MO$?{HzML<(%oV?z1?#)-yz%hErtg$`f?A zf@4gd^doF;+!QY=^k7(0;DOi-)`9d)=7FM2|AX}z{s%kL8HYwv{SF;W@teAu?0e`{ zvhUtcX})`w22}|2zgvf4H~T>UgZ@4Nset{*;70nK05*6<8tb{PFVuF;Mtb#C7mX@N z$`oEAJ7-+*@s2ykV21q}5X?Eli(;OMjPpB_67TzGK^*<;s#yA2@E^_%MfsfHA4$9L zR|M_+t8nt!Z&8%f-=de|cRhyP=>q)^`uh*)j5q}Pck&D$wgu7|8}5i=>mOZH4CO3MgC?Q!=TXDJ@R8eHwiurm{&zd`@*gj@OHi+ou3O+l>ng)G+i zT1UA0wV8C$Ya7+fSB{49FP&{dUlQ=_7er6r7i1smOFuvFmx2Cp;q34EI@RCfO`#v@ zZJjUaZ6}@dZrF$T{s4{m;R4nD<4cPBhxg>AxCtJ}S@405!XE4a4`M5HXY^d-!`9ss zz#2Zw@mEf3iWJTm%4E!#tHsY)8-z{US+S>aINuq(D`kf0;rW&9P5jCryUubcgl|z~ zmv5K z!fgzz10SSpPDU{Qw~AQWyw-}?d3}}8c@tgsyoHJHf{itK!2vu77f41B!GjGLB8W#(+&e;NOaxzETfnB(zI0+ue0Z3*{fCsVp5{9*b2e9fs zhLwEg=gUPBLMcdAEE*|G2O)Ju7SdMtK?b^B$i&DUS(v#XTT2{rvW9LD8%Si>SRo%< z3rIjf8UteVaS$uWgi%=~5aX_c81;HfUDUZGhxWr6JP97q1q@pcK1|&m3@dt!VOiio zBqBkf2qY%PMbgU|NKTG|l$1$GO^txGHE~E^2T~cj*2qH70@>=DA)J9Ray2jn-$5VI z4Rql;TN{PxYod64b@;-r3K3paRI0laJK+orodXXD_MqWE7*-5EOvZBzi~EN0hk_5n zMwl2K2}yY&Q5jbxDUU-kN_I#=#R_~GGo+zzjC3>%k)fs@GS$>ZR+^f~UQ-RZXn>Fh z(L#mmTqay)^3-JDI#CA2ED3T~kw=bj`)zNgft)P0;HANOhydNrE?{jr1v zgd94cNof->1Yx0ZU^fg*nQ}+Yr-8rR7F-^<{l>p>ox{(#{viA!e1kaAR{|n@c0+DU zfk^NSPI@ZH#Yi2wLo&kyrhIw94Y()84!N^j5Frd2qLN9-Dc=)0ReB=aYWTU{6FGN# zAm>33z9{2c8yx=iQx59`-D|1ei7k|Gqz=j~ zX%pqE#}IkObDTWwxySp9*Aef}-sikNk#BjuC%^D~L;2$IiZbW*f{e&dD2Q?&#{4V= z@`7h@1@D2Zt@J-OUZ8~<3c~3l?%xexKDc>54)G^7g8a=pg*M}rPygaoN&oEC==0IL z&F2HToA#bEKzm2oMtw`&MSVk?qP+GwLwQBNNq$a$?)`-Rnf!=8M|U-ZsNmr zA2SMA4;fXg`~FSLd(8FzcbL8Yx0yr!w^$SYH(C4quLqp;zZP)S{|fsF^AhJ1>pXjo zeTK6TcrtK-dz8DtTM9%2Ps|6je-Qlh5vv3G+XQ?F8|9ywF4|wxJWSpu(Cwc`1-d^9 ziJ;vLOk&<*=dy3G%L1>l>jJN^*92bXbOm1GY!18-xHa&6;LgCa+{4^Ix#zj3dH1;| zgWmIw^L_*$4*DH(Aoy44-jJn1KJX0wupbes(kNMbIm$AUUdXqWpIzys_CAlO_ac*O z`7nj;d^uPu$^HOjI?>w&{__Be_>ZXZi0?~+d=+F62fVph6)BXV1>f@xCFe>kS9~sW zRD50Prt!FtVt6NyWqUO(m~=iqnsF*BIp}zJPWX|qlBlV$+USE}&Cv(KI->W5Z;IX< zzBPJx#IERF5yzr;L|%#69{DV0JaRgAG-@_(IQnbcmYAhMe&C4(fe(@h$?sfE_?~Yd z{;kqX@_n_v%(H5O(!DCO*7Xv9s|$HNw?EP&eUBt2@ZjrXrJ$B(A=CG1V>O*oa-lW;q2W8%BCuEbAi9YFj40rH1uhy<;b zsRR>K)R)ay>j-~tFk1Ga*;4f08b^t1O&%)e>U>R3RB@aRmV}abt&C=mWhF)or)4Az zq~xdeC6#6LB-LehC#}uekhC$YGifNRBY9_5d-CzDw&d&Ct;w&l*QUJBS_8DCyw6+; z#FSqFdJm>4DZ+#dHKDmWZNU%C`hri^nF`%*vlF|tmZ)^P$;a?;U4X-$s$kEF(n#iT zL44TeoYaKw%$)Sjw4&_wsa3hHsZDw7Qake2r1s}Er%vQGrB3BHrd`f&NPCuFpY|rN zKJ9I8-Twh%gC7+ST^ac>6|z!Q@Oz_%z?-!?{14j=1g>}5ik@HZu5e;4P5(d>%Wg+q z5NWh3f-z7Q7t&pr9M_(kk-j!7KesufY-M9cT|s@uy284Qp2FIU(W07+{Y6z7=ZY#b z9u-w)zACKDcvVoD@p|P_pg=eaNz$UI045REC<}aTR^xlwronf2gBIVFjn>P~bh*hL zSx?p3yOwD)(Zq8fst@<=t&R!mEK7`8Ta=d8n4g_nn^Ra=m0eX*k=0aMmfclanmtrj zoV~lOF#A+lLH0fP`Mjhs`$ch4_RFHBKtU@cQ7Uu-6w3*sIz|33Ym~5O9g5hk9yRP@ zuch$G9)irlE{fL9c7My!bzDM!b0}?NLv&zkO?>p~@|4sXNDY;(%r7p=D=S}_S6`Wz zx4tqbZ*ygK-uBAO{Nt4w`M2Qb)AFpmXJy&B&r6m9g-T1H45?+POjZCj$n$+{mBk)y zl*F#}DPw2*&4rF`c9GuO>#Z@dfnh$>!Exzr4WV?jM6z2N<09&6lak9JHB?ZVU6Nf? zSeaf}U6WF{rZ%~#rzWvztR|u8aCLmqwd#bT$CZhNPb!iNo|Z2KijWkCG=LDQgq!%L z71*nGaqLzvOd}kCG{TUH;Gr!}Qak%S)kZh@n)G+GaT_{=z1FowFdNs#gjF>qBo@`D zWM|i86em~aSH+c=*T$5uUKLfbp*FH&YfVJO!Rm<0zp5fCo>WGaJ*kK;eYzwlMjSeH zBm_~76yN8yV%Y5sLfF;K@J58;71+^HBmTWx9VI4)NyLt?U=9=+dAf^uz8fC*E!6vSv$ya ztKS^#Q_&m2E!Y?noza<)8oxdzFMM5gaq!xbvY>VKrMz{WCA`+*Vs7jH!ob#x1)SDr z1%Yc`7IK?j7A*xz6&6NiB7Esae9Vw*ggBQ z19~rJv3j0mu{ONR4(NEBvlJ*>Nbq-!Aa?I>?b`#|6SUt^(7$`&4t)Fo_zwrognAC( zq}T5wsWzF?XM7D z%3pE5QdZ+b8rECpuhKE zKX?%1-~sf525$!+r16FjR`pm0D|w5|FD&>idQuJd--QdS~^MFT02l^v#JmrzE zyVv7DlIN31lE;%2BI#*?JL%afH{!Dn0`d7U-u=aX7x$N!oZVi&aKb;Eby_ZzIsTjv8Y0Ob433%;lG>1@4!ZmBVkkmEAnY%4R;?%6cK$(sE&?g$1fLH%Dz|=BVG)3{9Gv zqQfRj@jqt(&fqBIAbP+9Xn`z76=X5;A7EJ8CqBM7BqkJ&WW)lI5_k~m;6Z4E2VtO& zMYF2qz9|SLV-&1!gyQrJP?oM9D$~_LjXK(>U0WM%UXuO$ zFl-Dwh)rj~1A-2fYVcuJLKY+C35G?_VEn;|Unl^Hh|-Z1co4GSK`6>QAvN$Iv{bE- zzM45QQ8z`F8b-)YLmxS7=pdqo7WfVth^3~6LRD2zyowUaQdUGIO7f_7Nw!0WQ2!b5 zfWQZ;y$<#7LVfUIB41({7km%~_#hO7i4u{3gfkM6wnq{&R%nHs8Io5pLduE;NJB{% z=_qRO6bbtl43D{zVEXRPIR>Z=Xpih#?U4OOy9N7xy9I|4+j)oWw!amEeGIIgSgsGRNPzI>%qQHI8#moen>pH#z)p9&-5ZGG_nHWw-q- ze#-7E{KQ`=o1(Rv9FX~mjL0xm-B#MX9nE;2WhQ95k|5o(n@hI!bxK$lw|lmkZ$vm8R-1fH_H7XJ=Nnr zeWk};-%8SL-$v3c-&WEMzYV18e*L6tep^Xb7&}RSF{V5&G0u8i^uO(Kj``Z-jQ_0n zDd;^r>5r&KnTU2|2@nD5Q$YU*Lg#s;CTK1L>4iiK+1W$~ zZZ}zpURRmfn~E^;Ez<>PhxV>IwEW>QT;f z+7xG+evm!ydw`An_HYnmci_K4Ztx6#a?&VNNfO0tKpJ0P;%A2G^3R#Jvaizds*jS% z`nO}5R#(G=2^WK6yw3)v(ob{p{Z4Yq{Ei3KF^&bUVH^qUWE|%9F%EG@7zepK8T)xh z82fk^{r3bt^4}TskvSPO7cdco0=5Ms_UIBI5Ab1FGEyi?Q5>Z}0zO+;^jp5M_`8+X zQcv@pz}X$#%GnvRk+UOY zkTV%F&Y1{3z!?wyGjLnzoxrVO@3_NZKX_Zh=6U_$^Ff=J0Fgm=2ZGj$hd1YBsxDht zslDt=vEj0prRHJ}N*$NqDDqIbnCEMJDud&ABq_{ue++zGj7;V1h{)wlgqH-5h1Udc z3vUkI8qpp+60s>{C}L~KmdM>91Cb{}`Xg_I_C>u4?TwlZ>yDZW+wgyYXs{2#QsQv; zzYOIn3;inA6#P)3C-k(+MEFj%o!DO$M8!XgeGHD}1=tXt()A*N~-Rm5~lU;e4gugRs?N0 zEsouvlp5NbkQ3P*UmUX`zB;xuzA3gnz9VjZ{N}jU__4Tk@dx78#Gj9EiGLK|9RE3f zb;8%U#)R1=LH^Q`C|XhkWy%PmA_e}hRmyxX>s0yfuGYk^thQQqromO_Xe~u+U!}jr z_7X0Bq#%sapA!?%osksUnVJ#Zmb5Z{T~c}Cn#B5~=EQYLs}p;Y8k0tn>y!2**Cm}! zu1&fJd`PZInog=tnoe8_6d)ym;v|;A#1sLTUd#8nRvvrWD2Lr@QN=E}>_rRnwA z#p!L?h3S3S1?dyn`RPZp^U|;7#UP%T7)lWrLIsk1->W5H z4Meb8YnNe{*DGMBI!pzo+MOhKwR$R#t)UwZt`4y4sSk2*uMVfTl*e%EiW4F$SEeQv z=VoWD%r45!&8o@E&RUb7nbn=2kv*E9mVGckCHoTaJTEo-T~2z|`)nX>w$QwE5`ut!3GCHS=88HH1+cYoplJ zm2nXzkP*r&%*f1GnV+AMUtW-u*I1a4-&q)!KU5f-zqcSJ|6DE;njx58e&+N4R8o6HwRJDd&61r-O(Z8T?w(g&h#Ws zM?p$JM@>pV=elH8=jKFK=Z*ws*Qq#W=lxhl$A@@E+e`wZbtZ8!C>!H@Rt0O&0uvFs zK>I=lU_aC!9|!HZ1N?_w2K*g6?ZnpXaFwsy?yX%hPB$;u#&pWu%JE1X4)%@Q5*`>d z5F5tsPmN~w=g0W>S4A@h)KFl!qAU*p;v5tdU*qVcu%jyp}uc+8hR4d#|G0NIaw@ci~bd7{; zOwjgVM!-ZQhcS`F^PR{IqEA%t=o2l0KHGaaKHDb(XxopoXxr~FsT1#6-eYrY@3DE# zVqTR0?;4x~{e2Ad?_T))V^DuHXwbH!d|2}dMXc_mkzmD1JITTmc%`gkB;BN=WQ)kd zbX?GsKaq7PknDFbjP7$V-j8}P%a3xXj7~YU+J`)~kw%^xqj*mpC3_$KkGI#McitWc zeo{#X=4p%hdkrqNW7tX9gM;8fOhWxF-~)Am25f@2WmH`d!^$pc@)cY%6Un?}FOzr? zuO4}UXvjO~ZOuC8gQK5ix>C*t5xvhwlf2HQ6FturxqF_kck?*kUwVY_o0aQ> zK}*+%do2hLFPh^YJ~wl|KV#{1@4MAv{@#P5a0d2)2Qdcg-v{;Epne_rAjRN=WIiDj} z#NquaV}}pzM)n_u4D3Gc*SGt0Uf=f9Gd;{Po%cO6=_2b!T>x7Qxy=zY8J>*%@h$J3*n_^ zi2T&xl2}y_#i;1O<&YLCQ-l$63L2+ZQz4#0v~EE_&`d-9klG?WkiwJ z3Sq>M7C^k^d?;E9L#aT9|-t~=@k)fFi(udA>16K`X;G>E3xj?Ko($CaE zdc{Dk4$^N1+I5gYH!z@$47O?`!|mG0XfJR?3mN|jT-RJMd93-{^u5NM=~s;(roYv{ znIiRB6Qudo1Zgb>8NxH@8Y&?p1LgnmV@$2pkSSghnNYQnF$aj!MaJoR$fyvg23G4K z<2GQUE;88+4C^A(aUEp3TL+m<=`5I^)}A-Ns{PC2q1F$}w_4vUziNK9{G~Z z0kT+Yuwc=tKX187|F`89{a;qw^yaK~>ix7nsQc6U58WR&f9Za+eV{XA`&#>p-Hi4} z+h5urY>@7I8>Iiv1{o{{8EYyaQ<%hHt)+hi7qzsq^mk>d4hy2eLs1 z3r<{;2RHhJW6*7%|4ZR2~MFHLTHOq<^%{j$1FLe|%a$o2{m*)0ZHLhpqO7O@vAj;cX?cbE!s-%b+V(v8xBXc%q^G-_DUZE+_YZ#QH`JaaI79sBkk@|QtWS2^X+d^ z%k8gI>+G-6TI{dT+U+mXdhIXyY;m~gGwyKSXRpIK`f-Op>3=z#_I>Pl()W|oG5Va# z5jw&j_CbUxALP0iUi-szHGrptN1LYK3CmH0D$4fxZHwfH|6o%oZCKKyaU2>zJ=4*U`SDZ*j@bA&_8 zdxZVWcdmPxKiqfwBhoHlr$6#o4044vph-z09=xq38Z!3@+M=_`hGK7%ETx{rgzs!4|eR+A0}w37A*Y$WYt50Lh-$2@kk z_j&AOpYqtjx#>B}d&T|8ltZudopMqYN)SRphO{C(qAhB8%fVoEl2( zPl~4X#3wN~#AI?hqYJq0(G|S5=z89|=ygGBqq~DzqKAW;qjv?bjy@6G7=1IiKKc#t zJ-9AX%hUu@+GnNB@)>Ea#8GB6=?q&Gr?n3&Qkl!Jyj-)eGRv+ z46qx>4tDFwh@^HV$1__KQn_p5bAp@WibGe&)r2*~HHX#5b%w8s8wjtB+a6vWcO;@J z?rKD3+{=iHxY_WExVf;UK&)kO79eY$DatopD2m-L6T)s)31gR5f%dOA5tv$qlh|GD zp)^+Eqc>E-vffm{BXs75lUp-m{F_sgxD81ep{o*CMpP%1M^z=%M^_}YMVBRPiYZMT ziz!Yx7*mvRDYh{2SxjN#Omt!5_o%|eACZee93c@Dzf52uXBqaYSP;7n+V|=z(Ebge z{a1tbZ*&lwtS8EEt)c1kS28U&mU5lj3PZh`^P>Ffv*Nkc>8WAmsX5UlDaCO`$u;o> z$!ikwlQ$;hCXXcKB=1YiN#}qS%|}H4EXl6*ollcxyx>8plJ4jAJkFB!L2O|CN&gA(5v%fIi)#C;RTr) zvAO9h6SLAQk~7j9Q_|8qQc}_fQ6}AoE@c+P{tuJJ$q#|7#?%1FgE)j#isxTi3d-=x-*gZ(QwX+*Zf7Z>kA$udN89 zm6t@b3ku`Ivh!2pQgd>W6SIrc;uij zr0A^a#KoWpK0%bhhdnNad-od9T;M^R0^M`CO%&VRp@ofiSP5-ek6+%~>ZQ`L#@Ap? z6U(N)flH`d6-q9yj%4PP$A+YqB*nxPW+X=z6r_i*tj-86Sd$S_(3>7yFrF4va4eNq zcr%4p@HQoA<(K5({OP2{pcqWxXAbs1`*0O>-&xS#$3g$@?-ateZ&1TVHd+X7-ry{` zq0>XDwcST|bt}`dW=)`TX;ZLgenYrl`l=XSe04%pL}glHaCu%Tue>6STi%=+SkaTh zsTfP*R2)fUSKLSlD1Va>P&SjuE}2PK3`)TGKjdTBZP5S!vj*TnOm%_gfE@nlCPi$Z z&xF5wlcU)BUZQ++H&ts@7sITggY8(*7DUWi8|IVL62*>M9TyhdkQx(MpPRt0uSf`} zZ;lUW=#FDGY>Qzw9EoN&T#xdve;wsl_br-H^DSmEC>7)TdmpZ^g82`y2gksN+1CrY zZ!^3VVNeF^8PezL*kUWPX3%v-eZRL_Wgp$BsF!7%)6FHMbcJ}wbVT@vu8-vfwx)!! zT5}_rt>uyatxe(nt=(aawrwGdw!^`GZP$Z*TVDtJto;_^)AB2HF+Z}ve}D|c<-gAX z=C=0SLF;+#Pu)1nB5zm-Sj_X4@ca1p z8mxmgfc4)8^~XoRe*hn1{UkruvQr7G-(@UNwbNd_c!#T8?xdGy+IE_8+&IHFd@KOZ z9p!m2w}w-Fx5m?bMzVZqTTAG)tqnfZtz9(g=vE44^dOl$dc}*p^_7>`@K=iG&^&c9 z56WQ;n!pExGjIqzh#ina7zPcx5qyZ|eQ^JLPztM<(%~yQWVtN&kdt)kK{w^N175me z`)THZd;RSF_ptHwJ;CnOJuxKmo^+D;o+6_6o;r80z3pzEdxr>~d-vl#_Fi`J*z?kv zxce*KedoOEVs60i!k_SaaHJ11An^IOfd=o}4f_88=zq|G#U}-@{L@NU))_;=l+)JY zv8SEn!cMuV2maxy&p1gnr=4Kfc%KM#@H`peNIIG9KsuRkPyD0C&i#*8TlZ4~)^4Zv zSh=3MVCnkDa|^XfS@hjKXsgLKGt@;{z=#2+7JE3oZ0|;Fa#M4s6PT4yn6~V5XV6Of(~B^ zIy?n*c+_nnEab5aU*HoBA^#@^Vzeh_D?FdrD!4syR&{wo)WAKZYC1j(&~$hnrfL5? zQN#X4p1R$OYBk%JZK^h}wy4;=-luH+`jV2>>!*rVFTebYBZII9u>aej{wAou4zdun z;6oIG50VD;qyK|pyoY>Pz(*;5zmH17luw%Cq)+T-RgB@HHkv<5Pj zRz)__O2|oC9+8&IAi9(k;zB4fRuV1?;gY6Yd>L8|U)b9J#Rz0^HbVU-s9y&4v%m+7 z{k#4{4D)-2VV1C?QpXGE$`h zTqUFu3;!)s9;p<;pQ{E|%OkZ`U<1%6kJN|ckouS$(%18z)x1k(#=K8u+G0rgi^Z7oXUknm zpRA^oK3bnq`e1!s@vZd}#aA{T6kb?=SAGUY-cu{2_QVotEC%Ul$|DoVK3GEUfjvxw z#KGCX*(f6iS9N$vlIAa)K+W$q(HdWE(ln-R3N$|3RA_v(tylkGyGH%JU5DCRyB@VS z_5-S~?MGE#IqXn*>3C4(h2tre=eTRiPjF9^AL2f!+;{w;e%AqM-gZFRw;YhpVvr&9 zepo^FzyUJvZZLm=1aF)tS}Gz}XVsrBUYavbOzls&Fzt7^B<(l29PL+5rCKkXYqg#` zH)%d~Y14e-vO(i9ezV3S{D}Gk!le3r!U45=gg?~o60WNM$Mv!L4c8A^R|!9KFB6dd zB|I{?fJcUa(LX_E;D0#4owpnG9*|-7fe*}ppqa`3AloZ_@o?98=T6so>B`f4>Kd#6 z$Sp(ffm@;OJ@-o8JH!T^+r+gxw}_oOH;KJE*NIznu93!duafrY{zW>bd)ecn?nRFW zdS^Y}8JzL>Zgh%-Oiq%J*$EOdKfVZL1KJ-Cx|a<7_6!wqm`x(_E5La97k?YMH*~zp zQ!-imfmeY39j^$(n_kIA*SvC!uXvRhU-qsszUaN$_=0z<@j34<wDE^x9>BXoxam{lfLr~6LjP_{=eYh=l=)cm%|#s zT`vcw&W9-o%}1*VeU8x)ei3CNaxVh>hY+IT1+I_onE;OY3IA}Lql^UmDMqHlK}Mm& zeny4EUPhh69{(1{UH%=8JN-8~PWlf!ZfEXr9A_TJjWI9cMp=(=Bg{|EL(DmsK_ZCQ1V!s;Fw^WHH`s13 zC)#NjJH=%(AO}CeF2;|utMJ>{jf7G5I>HEh17VmmK-j_=Ck%29xb}1YbnWBZb?Xg$ z=ibBlLEH$_el{!t@(_hHuuK?5NC-gBJKuDwEcQH20lSl~iT#ysDSRdczv5_uxBC8Q zhVicOK)Z>MaF@}bc(-9*I&ljxpESrTCH3=aNqxL#(x#wxkDj1S9^FA(JvIdG_UsBe z>Ddu<)3ZJ3wO1SOn|CV@d9PanL|!I_xWcdoqWp6y;@F1_aqLm11a>`J6+4$>CU_#- zS?XZAr^>D*y1{rH+h#Z_#Cae*n%EbX?Aa5V<-IYqklYngLGBExr?iKxqpT0vNNEk- zLR}X!NnI0il-3e*h1L}Eg0?#3E3GkP0YLu(`3S;32nnKuWqhB~g|YkDg4nHGxc6VF zh@CDl5jeaOC$T4oq&Sh`qdStqvg}XbIrYRw5WAw{z1t(xsI3vXKI_6u=xf4je4E3Y ze4D~Md>g|z`!$4*`_+XXVyp_k$fyZ_0!%Zi!{-6ySG5S_$G;3k2=dP*3t~?*`LNr0 z{Mgk3(Edeo*zr;${sYAhVmk`m^K1$5({p#y5s$C$xuVB@BdSBus{-#UBq#jlUI^n(!_p zHQ`5aTEcu#TKqikpCB$KxR8Khk3jR?EX1&jpm|PLgZ8f#$M)2L_OG)N8d~KdwW-=u zxwC?B%J#DaorMlatp)CMIu+j87hq zj7vTe8IycHGB){jL~Qc6u(;%3p+Lw#K_QsH_hi_ITnxMR_ZooyKCue!8|#;0J65Y> zqpQsY2O6Cuy6Zd?*4NUso2&fI>dH8dmBqpC#RcKi+`Jf8dUj$^Qf5X(TzWxtOnOyJ zRC-HHM0$5jc=~8eSjM5~(2T#LL(^YIhNjO(gr(1gho{Yj{Sy?9@qdJ~a0hhfWzfE- zq5hFZ_`VPQ7-KCe*w7kNflV!rVx3JyxwVZH&Bi*0adi#buC$WpnqL-1&M1!ZPb`S% zMdznRgy-hQgyfdR2Ia1f<>hw8aA8OVH}^nPVBV!jPVRGHCL%EVXE=8eC>rB?nhE<* z4EYQ2VNO8(gDr6H4f<=ORRP<)-iU8wo4rWeI=2-~YshM=ntct*8(G$c^<0ot-X>us9I6I0{3$2ujvT(uN~qsYWH#dYAm*U#O_DF! zNY%{m@-t5F2(V39&&5Z!hI$6AjiR$#5}5wYnQXu2VzysPU4U;(JIl9ai$A?(FN5B4 z-q)x3sV}wZ3xl$H-k;KlnE&J%?86Q41kXYiA0BhNzj5TaA z=C9sjFIoy-On$$YYQ|Z z19%auwynS_$94Hi#;k<%$DAcIwh4&v;VbZ&za9!hdn=?==7)=qUI=d!hdL z7B~lxg=im#`~OJ{E8QiG73@~Ua`%`Br0=m4P1ucJ5w(k?61Qzv=$ z-rFM`y|yRgJh$gNdQ4V1kS5pKktR3W5+`?ByHB36a-V!?={E7nnlS#$mM}K|UtEUY z#glLb4uS`<1L_ZNgEfHKE#Sjcfe(`lS}^U1G?si+n=kgLxlqJWdkNkVyd3K=NtJ$> ztV5piHH0yLCd8>QGxw{|>$@EJL(k>#eO>3N4|=$R zbB4HszyFIrVGj;>Lk0xuZySSiume08@B!+;hbcY``Wv)p^jUr^?7S=%bU}xobHQvG z|?EiMCKM0?_6Fz$*eD<=Vkb(FE!{X0E2H`Rv z#=0Sl(eKD$)VrGeo_CFwx!<)Cce(4h9Cy!6#{M2z&hEayobCN!IhzM@vNjL1Wvm}o ztgw2tR@(A$|8k4Rd!)>tT#zz*@>J5~;k1;=!>`N#$wBbpc0&CTsNW5ry#+pdCHNq@ z;DaQc$FQ)!Ff8CEWDxH0V_vT%_}pJB3b?${Smy9XU(DvMnWW`g+vR5OT%=9jc}N?- zr%M}t;4U}#7%ip$F;i0SQ@Moh=QZLwpZmqMzw8#(`tql!=JaC`&FL5a#ctUDZBTy` z)L#c#jOvrn2Xh9)k}qIb1o#l_TTuT#h7q6q?Ewh!IewEAwEZqGZ23b~)bxk8nBfmY z3B8{d652oQB{k;=5^BG^B~*U-i!1&P6_cM&7L}VX5|v$O6p>ljxNHR)6_!SafFu9K z1k~?``s-osSHapBz}lx?g8J8>{%xoaJ`53j5FB_hw$PVii39{qkf^W$l3u2RrA3V)DpbOa|GDNg-Fb9HNK_BesYDie84HEMW{)2w|uh*eHmh-v43?)bE7) z4N$)rvLNY~!2`R2VVt{A{}G0{Kf^G`*BEB`5#uwN!}#?PCa4X$5OpLVtb&#;Q$P|T zvPecm8Yzm1BXyBwNEhmwg3nVrWGN&?AOq27nPUBs(F7aCJLG`b|eYiyGI zra36_O>bEm;@^ECa1yGDshu#mHC=8QLizeRuF>d?70nB>zh{PWGp6rp!0pLg`t(D(M-$ zhUL@xYnFe}Z(sh|pj+ybLBG^T!x5iZpUEll|?8 zQ<$~$Q2J!!uk_9)ROyvXg2D^iZ24!l#d1&Vs^lKoHOM}+Un~2-zC-4oL$AzThe4S; z4r4O69d^syay%k)!|}Y#b;mohf8k!sU3C1ac+PQN`K%*S{nG)dopC_w{{$KTT_3c! z3&g5O%Hohmk(l=|UjEhHR`xyKRq>@04LlvL%0s7U<$KO)O1E8BD&BG_SG?h}O7R-L zN%1PaP4NnTgVJSupVB45h|&eZq|$l90j0BqQ_6q3UROTl`dsCN>$LhY!f(wZ1f+Es zh7C;-kj_6rmMfMcM==TH0sZzg1rg+{F8ZCWFaDltvHZEGliUNMm(p!_mg;r)F!d|$ z3F?=KSsE9Jg&OCG6&in%>NL)fS~O0P)@%Gh>ee{nF`#kWW1HqtkKLL_JdSBjd0f^y z==oS{pXW#2JsxxVyF8G=P7h?bV-d&>w7)CdH`8F^J4;Szo}(i4nXN7Sf@v&z-_K6! zh7VExGKH>wj?C3L?H#3i(mO@(I5|h}D7jewFu6*9irk=oki16!0Hs5JAEj4+FJ)MN zH)Yab7v+$_4(d6BN$Neraq2tcZPXv8TPetF1Q@0w^M8VzK>K?^&YKAn-+9vf-$Ug2 z--M{~Kji5O-sD<~USzwa5eAxGt`H=5TivhpamYe-%tv2}~>rKAMrgssDAS8x- zgoF@RgnvFvobOBIa_m{89CkZG1G^kyA$&T_Mfzxvm+AqIpWbd3*L0E@VL9QSXtm8h z(|Xi@rS*t^xy=xBmCY7rv&{f=y-h!}*QSp-Y`clI)3%3o%&wbt#cl)ZxqT;d#-W2b z@7T^n{{j&OL=Y2l-XTJK(^10MvlwCQZj1~&e#UG6|u|RmdEzHx5ch?Z;hSeHplMbHpQOuXo$Vz zQ6Kvhyy4cxQlD^N4CPPI4}|=P9Nb3Tc@6c(`6AT*#iC??sRnIFsTq5w$We4-frtFM zJb$g>>@bs+=`l7vsYy;9Ng1x~iFw@CgksO;gley*gch%cgl_NpgkkU6gpEEm3G+Tx z3CDaZ6K?ueBs}&hPr%O436wYRS_~CT81E8M`=kE9S%BIfb>C_D5QobI$?i%uvbEBL zXR6#mczubx?8q`d^@02l!=5EkRx2{&om$dT-5OJ}J?oMSd}@-*{i>21{40}J_?IWI z3MfrpA5fA!7g&^hD6lZ;i=e{fM}dXOFZ~OXDZl@P3M2GCKlMW{YR)3$05#8X)PDP` z_{e+>o;#>T?O$ytI9ll@H3&bbw+Iy1w&JU!RHB&|58D6J;A zFs(JDAgwPnKW#KLH*HJklC=Gy*{PSqveJGC%})C>I6LiQP<9#>xELyu(0)n7SX_#F zs}wn?LJn$C_tvt=T!R9cYBXT1ueae}UF#yTvdUYjt2|Jrtt8B>VOg|&b$)_tX>O|b zvh3`@{H%h|oXm>w?99f9%*-yls$wKEEpsX|C3A0Na^{7|q|65q$(g^0C1-vJP06G} z7DL70xdYVRx8TEEMEob}kOTM-^Nn;e-7HHswCK{;wpj5FHad&-)O*S=ukqJxsthr% zEsL}%D~@w1EKKswS(*`)k(V2unp+Z;lv5Xzn6n}#K6fZOE_X6IHg{KaOzzpJ=$!kJ zF*(m8VshSv$K+69i}{#9$agvLU@#8O)u85UM7`01ny(eLce^ARTcJf8UT)6nZ*vsx zY~jkbH2SI6*9RL^)I?YnRmC{vmM3^-l%xhE6=jFV6&6NE7gohY6}H7h77j#56i!5i z7w(7*D?A+;T6ixawD7m^(1N$&VFgs!VqPU_z+d6U5G^Rmn$4r^!>+A)Vjk-$atnpS% z9|_P(7!Eax8j7?E9gKGfSe3@{9msX{87Oo09%yp$9_VxSS~ce6xoVq(=fDYjkAbi4 zxGSI9yY;1S7XCSw^l~iVR(5>I^tD zUHY7vHM-6-vpP;QN3Vw{5&fz#X+>dd9_~#J!hRvw`PzQHl zJwh$Kh=N_H|M#*<%7Pq8SkR%zESR#x7HowA_c}}X?B&XF_xLKg>|txF=2B zVb4-E`@Pkw_IsDB*zFxwvfVqaXuJ2Ig3aC=^47b5mbcpVQqgkfd!@x3#~9d$zMqHw z=4sUb(C&qHGqlU#MdTbn{eKko=LrE4a#Dc=p46uMoHSv3oU|5nIq4+oc#t2M1iU!peSAB{0kqqpU9}(e?_qc_#|epq7vPU)bKJ2e#pSvTak{Qfx4Uk@V|BxV z*ZhV(zv&Hk0h2HN1&zLp6g0Y-B4BtkpWonC4WIs3oxFOt*YWD!o@Z;{Im^Jll_l}YpGV|XG*jTOOeX*9 zG6vt9b_Va85jy+L44w7%AdU6*{J+=&{W0kGAH*0y?4^i38?nbD_7KGG`4#l<5@P-x zA^JZMqV*dg>Td~6nPM>%C?SR{CBc-Uk|D9>udU7te@zAvR=@CXT7Dpz&D?>D8_I92NR!v zLkU7(6#E?rSK(P86WnIs6p6gqPy2WmCMZEQ+s_^?|>i z=RN;0&pUxp=39Xc%r}D5jMswO7=H=vVZ0JL%6KVsf$@jXZN>|sALzddKc_zxdc*id zm|{K=rg;7rN(}mvsQD$Z@1mrtC?%zj|F(wj;wD0g2MJSRae|a+mcR#*Wqj|%s(9as zHL+iduVDWr(aU-zG05{$at+TPlH)voNKG+cNX;>yOYLI*CViOstMobMGwGX*C(;iY zkEDNN{3!E==LZ>z^-zXlKaiz(7eh%wUl#8>ki#2mm6Sv&6}*K}*<6fLbQY!L{Y2i& zMG3u;OA~x0x0L@6xpKbW?&=(1eWQDt^|kJotXsMd**A2b^Ig+>D{w`R61uEQ317s+ zU5lYqps$H%AM~;JftjozWuYcSnc@8t##W*)4Bf%zTJHrCOZw)I1 z?iw}le`DOvf5*6+@3!#(-!0=2zMCfN`EHm@@n1LH%74vtf&Z%M3I5Bb*Z41*eJ60% z>{sE_W^Y7KVsY0AQ%VB6$ovaR6aI$*YHus-JK%sf?>Q>*y>ZYM_|3*t_-9K;(Ff+< zV)x7=MDJK63*WTN6~19rBK(C_weVHzCgIE0%Y`pl_lR7uStWAbX06Csn++moY_^D; zvfU|i()O_EaodZcM{Vzl9Ag0T?4JLD(uep9(fLlF67w<_#*?^M|3zEWWacSK>HJFc*eyG3y;cemm!_qfsw_Y0+Ij~|pb zaer6c$o-(UflH}Ra4C(&P*!wa%8d?v7X57qA9)@sL>>l9klR79J9*MLO+!y91y))QPX=`A(#!Nth)^tFI_EbQ=&SXG|&W3<$or!=(o%I3B zb;knwbVmbL>#hx$&>IPu)mt5~Pj4vToc>_IHwFU%&kR=vyfN$#pp5$b{|)7g+TWkh zeviO?&KN?zipSgk;<5H1L7E&((xLB5u;kwn%aNFk@>ZM*57yie8m+rNG}&NXNVehH zkOHGMA!SCZLu!qNLt2anLpqIDg{(4O8M4l#KV;gZH)OYIPsl0LuHf5doxxAcRs_E` zUlB~1FaIAXchvqNsQsgHpEC({Zz^hyR604CCQc4!Xwr72oAb`4IEhUqddQE*255{% zN9eDPj5i*NNHZM>&oN&azRbKYyuzXDe8h5j_@q^P_`FqH*fHyt zup8FRVUKK@!d}@lhEdl43+09N_Ti}a;!*pj<6bK~fHUAomN40yqeiwZF=1`avKO96 zcb8q8;-@y05URH_F509wCdr~JI@7u%D&J;#WQlEiWVLN;WV2mMWS3o2=#4%68iHf)O#t&0eqPA@Bog(3)!E?Pj)O- zCNoQom>ctK1=lTcl^V|UQC^uEq|==gVX`72&ay2o)wVe{+rBZTz_B5w+^H_6-l-;L zxpQ^QO6SU$b)1Tr8BSTuewUJ%OD@GR--F*d#W5e8i(;sMK?M@pPZ$Sxkb^7GKaHB_ za3N}qLN?iobvaW-28{7#*8C$&IT8c8UWz^00a`24!;D%|qAeSe676ae(w(Z}b2%09 z#je=o!L2mD#l1Mb*L_+18g4=SX71AXJ>0zba~`>I_dRmspS$PAzjMotr(75FX)HWM z?R^V5xCs3d#mGSk?z0s$$(B-Cvaw8;K3ZzYJ5=l}+E?fy*OBk1(YhqWpdmBTqB=d! zt~@oxxi~q?tuU#;V`);kXI@gHS8h_L_mZSx@2sRr?~J6K-swrFebN%|dZ#7*=9QN8 z)&uWq=Pu^`r?HTXniDxVhuZgO8ESvjpIgDEN-46wN}IO2(ww!j+)=oz#9gMX$XB(o zAV{w!FWjtbNsQgHtVB*;Mml#%TCR6yTCs0>YOP;tYP)|*>VSVz>V$t{>URJ5)D!-3 zsdxP1Q=j?9r@rw{NTs|M^JgR>_s|blQU9MVMeSdSxqdaCy{JOXS0hf=)@hKzT2r3h zYJ0&Im2Q&F<=#rQB>~zMMWH4|1yMG6dGXGfIVm2g+1b8{S%m@dS(SmYSuKGvS$%=g zS!01wSz7}lvW^CXXWsIU$o$1GBJ;IxWG3aam?szuU!xx`AqOX*zYn$F_Il)?j*xW? z!eqEfmGn0mF*+M;`CIE;#OrH36)LOzHHs@j4D(APEVGJZ9a9RE+~b#K_(bRD2SntR z1%>A|1%>7H1cv5~2886#`UmG9@e9iT(l;pYiEmKuUp_&()W7&K5yvEsgY)QvBhcT| zh?=hj^-eQt?^ZstvQ3e6wdvE_TCI2+TbxCzn>=Jo8+=s@>Vou^)P$R*Rz=&zmnXR3 z^6e`gz>dtZ)v0}QNSlCDnd|InpPYw9#-)pR%rmMwRaENu5y%4rSIN^cG| zPHc>{ims1y46RLZ3#wV-;a^ki=~q+l;aAhe^{rXs?o%`4>Qi&b#k=M@$E*5h7tgAf zt{#gRs54CXpDC(YdbW*fl ziY#5PNtUcPWu~vUrh(@pTkO>?0Qx04ktm(5RQqOZF(U3cmW#l%p%*b`5*3fli zg}%$kYCX=#W*yGRer@NGYg$gjKWRA*{i$O=_)%{$7iusT(f50xKL_0nYtawuaPN6N z*8jr~DA(TOW_!VqDHn17DNk9iP5w&mlVPeZld&4klW7`G zlS?%mC#%#PC)-sWHVvxSPi|7OpIlJ1o4l-GyYUAFn~C3*tk-`~S`*14I}8{BJ1p2fJM0Cy^R8kp^IlSp^MNw< z^O174^GR|x^Et9M^JOyD^Ucy$^DCt+=Ql`L%Bcc}w2{^zY-iIWK=OH!Xen_9eIb_aqIAqUf zbI4WD@{qT%`JoUIv%|3>riU|xO%5*;GC5KwXndqw!05<2e#0a4eELUD^XVP_mRI-S zuY5WO-U#R%c>50v(BBUI&G3Lm5py46Zr%wGW;b#G?F9G%p+`{v!Vhr38;z|l2ouYT z3dHQ97R}_M5ySA3HIM!!XCA#vUM!uWT@(W$z(ksSdc0zx48t0^~sJ{_+JG86zVI2;%Q=lCIZC`j19Mt1hR|qk@ zK_{BG*+lKGC{ekqK$P!l(iHC*(-iL6(&WE&qsiS5pvl~ip-F$2MU#9`MkF4z6S40{ zi17DYiQvQ2g#Y0^!uQ|>;rs5tY=!^{^zj<<1!)E z@FI+E!GC}spnRVY`CnK>_L&HgekMbcc&0)Vd!|DZ{?(Kw@T&uj?>A2x>vGQOOf}ivxLN6!gDatc7wJJw2h#x32nun2$6bBi11TF1YQxs z`;lNmOlWk94o`y(gn%?q2717X5KLGJZq&d70VMz@BdX_LP;_-+ilHh@kx!@#QN#$o zhaG$mPk0_-Y>Fg7CWlFp5(Y(T=@e-J9W;vc(ms+w+6S_R_KvJ4Z^XRkX`;U;%js`PH|-4>puHxmX@8M*w3lQfbhkiv9=iLWcM^Knpnjh` z#Xp~*b&sL{Gk8R!Xp5l)@V-d_S$HloLKMF;&=sWkEaAPl@KZbiyc9Er{gIK*^NwD? ze8Z?<{Kc$iykxb~|6q60e`oj8Ua*H~&)I8fzp*B0zp^%yXRK}HDQkf|VI3!rSy#v- z_P68*_EYjb-(TcAeu{QqfTG_MqUd)QL5X4;TQR)rP#Evolfru+rL_bpNmD^e+)3c0 zs1M&8;Yju?p%m61Lb*K8g-V#e3fC~6iZ;`K5nDljEY?H+S!@;Uk=O|BC$aUkAH_D& z9*S?JeJ{R;JP&0_ zMYUHv=hZ0IIW>xnJ!N>$EP|4UBch1KPMUb*oSqaPrLWBQL06am4=oFUry3mopVa+% zzgLfD-&arLxvP=SyrWsl{7SQyaZ{_A@uk)Z#&zu;#uwVFm{)byFt6xLFfZv&GcW4S zGtcWDWS-MI%RHm^70*fiN33J|fASvD|Hywxj}kZt4(L&WpP^LXh-l(Yya_*>vcQfE zmU65=&D7aXjE(rdH?ZTotMAEs+aQd6!zht`%{ZHV*|?B>(WHWX-lUFw&a{R1jA;k& zY12O5lV(G_C(PFI9y6QdJ!&?`d)Rz0?;-P(dt z!DBW>f=6sCg$~&^2pzO-720pxDYVaarO<-iYN0)L<3hXbriFLf?G)Z&cSLx*-4)@j zcHfE2+WjWB#qO=dW;;r9lN}|s7|H-{fDI~P4xR*XXOkChg5)7blH74pp3NSBcL%H;Qj}ZWrIi>6Vz|tdf}Jtd-cp znUt93Y?GYg9FW|^IWM`<HfgODd)s6IU5 zNVvn1@Ps4btKB-C*K1-C> z`xGj#^C?#z^{G`}Ypif&vuR z1xBc@4MYMo&MXjSNI>& zS?+&Dr_KMNZma(vx-I@6besLLCzIcQp4+C+}TP@I=Se{`UFcVxIuM|hn6@~~9Hwy-5eEun?R&7tKc zjiL1>4WaF(b)o&HwV`XxszW!MRfg^{uLwPBUKV=SqAc_`^U~0_fHGSQoEh>E7u#Ei z{~|89C$g~m8|=*BAv2lS1u;vPzAn>}cR0;Sv_IKHt}DS$b$M*4PD^yOVN+D1NquCx zS#3nFMRi26MO8$#Wkp1@Re40WRcXX(>*9z>>tzu;Z3-h!+ANK@W4kos89v`w=SNUh zi+Sw}HvnVcI$}SUjU4162VmP0I+?;so$*|4+DMK$dm!6ExF^G1dU>jka!XQ>c6~yG zVNG1DS!GPJWm$BVbxCxAZBcZ&U14;CT|sn*eSY+yeQxxG!;l9$nq6L8u0u{-u~T+jjdNyPn{!57KPN42jFS>K$4QPm z?2;IJ!zC&1XXm82S58T>lp}EX%wrsjpB%bN$iWF{?}IC_9ZaDz9xDpC z*D@RawgQfLW1fe6O^%;hc~-FAvWy7Rywq6h?384OjHE2*)T9E}#H41ogrpv~ zxTLjiu}L#-F^LDzhUx=sz(j0K znE+W;u0(pu4d}~Dt$3S@okVJvab+qBd{m0^gLLw9!i=-Bqpeai6CD!M(_P}y^4w$6 zO1V*K4IYtcUEJ`r5pG!8G&eMDpLRNPRK-hrtK~^YwJxot%7Rs2=^#{D?j~7M>aDo6I6yOdS*T&!(nyPh zyg2*loD@#Pk|pk;*+txt>{^fD>=oRgCByE4OQzfcmMpmXFS+F6pZ(CqKkE?5AFF{%>Ka$D%1Wt2I;7mCan#cq@muFSzTwxUs~%TUQq2Rms9Dd zmR=sLmslEZ8dDr&6TU3TDY!7pC7^Jbn}0!#t6$-A7vI7mj&I>6XP?5oPTqwV9lZ-4 zI(jYr-O(%my^~iy^$*|ReDKMkJ6eu@s6#(sB0S!N3J9N_&Daf~MT|7Gs*>tfBSu+^ zHE&_Fvq)|uS30A?S1GA3P%EZ3%rLw<+9Iej(bm5r)4{i*z|p&+8noMcRSepBR!-V_ zR_w9%sJLLoE&txiz3g`zx6*gEZY9(|d~ylTqVEqO?w!z_ZbKCadO+)PRH7YlBsvvH zX{R2ou+xH-3wI`Cg`0SCyO(@yo4;B_ONdTzbEL6fQ-Zm7V}_+?<5DY+#wts0W19uH zag~{S<3>~W#@!}vjTekv8y*0@ zRO8r1x2PYE#7a$)v(ki-(QnI})W;Et?e&m~=JC!(>x$6v>WnkscBUJ;cP=$_ z>#Q_%?QGR|=^W5=>D;Kp>DZ<1+;LvZx#NMRu zs&0d^>MpBNHJn%FfeLk}!4@^g!Ii2Gt0t5kR_#=>Uv*Z|e&D`>?aJp$HhphZtouHy ze&!VVU_bIc5ACS|9FMEe572IcLs7OC`eS%A#W<71;f+es8#GA71|xd#1}nDzgrk7h zguAHwxQ`TPeUPl<`Y3t(^-1z}>vQC7*O$uKu5XgHS>GpZy?(uv)%tl!%k`%vEXVGN zTa5lDX}aF#u_9oGj|9xh z5#Kp2;x%W?aG$kiab}(P9cH;gwljVr)-z#ZmNRi;7PFb6=CjL0%x3F^O=r7=OlC&~ zO=h2(;3aAx`@;L1#*AE28*gB)zd zQz7$&c<+KEut$J6?v*3?STRz543<8 zCL~UT{NRsUR&f9qqMvZ${RxBqCv@~9B=-@1^wH&im_*)JaR`pYd8Fw!cm!U+nRp8*@(Xyf2xvneZ-6AI!ih54po=zG!3}YP6B2|r z#G(xuaA20w-jZ@?*Fm=hx*hn{UTCet?~mZO$G{|he+Iw50}jYRI3Q=Cev|w}egMzO zkK_%Y@Lv@9ei0NCD-amiiGfI>4T@-k7TRErHgM1eKjb0`E=&sCmt5MPqy+I+L$?XK z?a=Fn)=KCMLt_-bzX88LO&-FX`JU`2-@zgJj@*DZ^F8?%-@1o3d=2f}{{zJbeO|Z` zEW80xK%S2hRL4pfV>ZR(zbEX-^r;$YVwY`I%8qeq^+ghl~#L zfZj{)(+A17^flxjeVp8-ZzkU`wvjuGedIRdB)P@BPHysiPj0YZkZb(!$rVI;S&*V# zS_EEEWnLq>&{lG0HnAe>L`3zw36 z!ZqX@;U@C6@N#lnsGHmpUP*2WuO?rLjFB6no5&ZUTgf%C1#(6FB)Kepom`N7NY2Q- zpq-L^Pdh0~(NBP5vJ~SplqB>eu^K=bZ@gE7|DYz%daa_x`c1)<=aH-v^MR}{{hn+D z?T%a$?UvjUazlO@`9h(BTvewC8Z8>QK^@lR~{til-H6o${WaOl^NPem0h$G zsz+(ZRIkvEsNJU>RR5K+U;Qm}L7n2+3$V{G>ob%bosUvM?X8QQ2n+-n@AM@Zzv(D5 ze$>!o-cz+^+*0Gxzt9M#U(t%AU(imcpVP@>oYpB}oYJjgoX~Ax9MfxK9M$V&9MS7% z9MT(R9Mm6U?AM=S?9-oT?A1TW++%Q_xy#@i=Dfi#tZfE=vF8jZ-dO{RZ)OqDXH$Cc zA57`=cNQ$#?`DGJMjtKbOGeH-XN-MWCrl$)M@^Ghhs?5A2h5hT_M4Tm z7R;;Jd(0czyUp9#yUe@UJIn{z^A>B^+blM)w_41xXDt?ZXDm+hZn3zVQs)TVPnTWWaGiN&nAd} zk8O;=PTLfLdAlV7+w2Mjw%U~m%-Yom%-A&vOxrCN+-%n?IAuR1IB7p7xY2%EaKe6< z(763^p)vdG!lU*-imb8!Lv*$MN3qp*l=$!>C_}=42SJ`VqTX`BWyBT!gCkZ%IAcYG zt2TYVizV+a4o7giv#;>1bC~EBPQ2)5PP*tOPOj)i&N8tLoN}>oPMz3#PK($&PN&$Y z%Sy2|E^EX`Tqea=yKEO9aycw9=yFA3fb&4IpYuYx4_i|8awwS|ydLZ`lsTclg#H6} z=)(uO;){C63r|CMij)05>a?9crmQ(H2f@uA9%380fs*6gNU1SyqSPoiLwXH2PkMw~ zEIrJvlpf;NORwU#$qaCNWL9#AWcs+{vc23{*>3KBxi0Pnxeo5P^2@ouDYU!4Q)qLi z{snSCdyM$+`XT-h)V*Q2^B9cUJ5Z4948|K6gAI8$1=<=&%8ZQj$$t=@Z-o4wDdGwchIupj7J?@ew)r!3&pl=wFILtrLTJK0bG2mCV*itVW2`V~j^w@~sJV799%q zlwKJapwJr-q15Fcr_$kY>iJFap=~@kexmtCBMcTE2Roc~oO*&P9 zojMhPL%L;w6MChA+x3bAkLni%-q2qb__Kat;48htKuWJLfYSZUGdGMyeu2 zJ(z}C2cI*Es5O!#$+{Fx+HjHyPk(})U{{=rM0>QCd~;-=YD0LqW^HJ!PIYLqZe>W8 zetF1JgVK;PgW`~S!=jMoMui~*MoUA+jPpb0jB`T{ndF3AGg%VylSy{SOXKX2k4D)c zl;LL{qaVJ;G~*KDKMoIKUlwwJ@00ZtPmBN=LBAl-=Bood6sJlR>SIl)7=F4j+_ zDmp~7JTg+ZBqH8mS$LXJL3oZyet3~-UU;=xPI#+Xc6hIOR`^=;jPNZMY2o`VQo}D> zriA}skrMugc}n;P)08mkAAZ7EyoE7v9`PT^#yme4wayaM{+YPzlqEz4vz1A2wmxG8 z{GgU}N0Itecj>AmAEmN{K#gT_;d=QoF-AGjNv7G+Sr(a51(xYi6;`QHjn>IgUDioa zBQ^<9Q#SEYdu?K)E`SHtu~ENU#YVlijElsc;s4=b5XJ)f{|qz_<)I&zVvdW?u^h~^ zbNNVro;>Ny)1|fLnzI^~*b7x>xk{F$dnqhS^;geJ4$;j{j51D7h&NA(PqR*p%e9S< zE3u1>tFw!VTVWp+H)J0fH)$UcyW2iI_MBZ4)r_u=0Y+0p#<}6d=8;4J$Q0!d5HvRE>$OWB}Ry6z?wi~&XoHTST`NqJd=$R2`*=u7?A@vVeaV#FklzRc1b9Fc#!K!-H zJD{!+J3=-y$g&nmlHaOMmb4l((p#$3`mpMRT?C6l`%bZz?u~} z;3VL)(p|)(-$&B5KS;*8FH+vIFHyn1FI(QOuNc(J+V*wH*z~QHw(gsgvg$o5Y1MmM z!m|4pNsF#m(&n8XWIl5g{l5n>&vv5j1uIbkwL`n6A30csx@#D<_Zrlmqe3JYZ`ckP z)giv4rVP(fTbA2eXMX3k9>NZ5{Y7opgo#_Pi5It8lOb-orclgcO|7W;nhp`OH6y}i zYi5K@M-B@bkK7b69)2QdH1tx)aPWihXAU6m^U&Rdcvpk&mGEE&(GRP!Gw52>zU#3o z!v@rzlXSw}Bt~4PlnG}_mpD$DF>I%7d90>5Y>Q2ve5RWM`As%O@f&YS;WwJf<1?J9 z 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

eT>Ej|$^#4DT~8N%6-A&eavLcbNS&>4OWn=yg1 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

  • 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 diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java index 16b2a6acc2eef..bb5a0444fcb1e 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java @@ -92,8 +92,9 @@ public void initialize() { * Sets the Bridge call back */ private void setBridgeCallBack() { - if (qbusComm != null) { - qbusComm.setBridgeCallBack(this); + QbusCommunication qbusCommunication = getQbusCommunication(); + if (qbusCommunication != null) { + qbusCommunication.setBridgeCallBack(this); } } @@ -106,30 +107,27 @@ private void setBridgeCallBack() { private void createCommunicationObject(InetAddress addr, int port) { scheduler.submit(() -> { - qbusComm = new QbusCommunication(); + setQbusCommunication(new QbusCommunication()); + + QbusCommunication qbusCommunication = getQbusCommunication(); setBridgeCallBack(); - if (qbusComm != null) { + if (qbusCommunication != null) { try { - qbusComm.startCommunication(); + qbusCommunication.startCommunication(); } catch (InterruptedException | IOException e) { - logger.warn("Error on restaring communication."); + logger.warn("Error on restaring communication: {}", e.getMessage()); + bridgeOffline("Communication could not be established"); + return; } - } - if (qbusComm != null) { - if (!qbusComm.communicationActive()) { - qbusComm = null; + if (!qbusCommunication.communicationActive()) { bridgeOffline("No communication with Qbus Server"); return; } - } - - if (qbusComm != null) { - if (!qbusComm.clientConnected()) { - qbusComm = null; + if (!qbusCommunication.clientConnected()) { bridgeOffline("No communication with Qbus Client"); return; } @@ -291,4 +289,26 @@ protected void readConfig() { @Override public void handleCommand(ChannelUID channelUID, Command command) { } + + /** + * Get state of bistabiel. + * + * @return bistabiel state + */ + public @Nullable QbusCommunication getQbusCommunication() { + if (this.qbusComm != null) { + return this.qbusComm; + } else { + return null; + } + } + + /** + * Sets state of bistabiel. + * + * @param bistabiel state + */ + void setQbusCommunication(QbusCommunication comm) { + this.qbusComm = comm; + } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java index 9a2b13cb1cc33..6de139b1e6b84 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusHandlerFactory.java @@ -38,7 +38,6 @@ */ @Component(service = ThingHandlerFactory.class, configurationPid = "binding.qbus") - @NonNullByDefault public class QbusHandlerFactory extends BaseThingHandlerFactory { diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java index a34fc179ecb82..1ce96cf4ffaba 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java @@ -56,15 +56,15 @@ public void initialize() { readConfig(); bistabielId = getId(); - QbusCommunication QComm = getCommunication("Bistabiel", bistabielId); - if (QComm == null) { + QbusCommunication qComm = getCommunication("Bistabiel", bistabielId); + if (qComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No communication with Qbus Bridge!"); return; } - QbusBridgeHandler QBridgeHandler = getBridgeHandler("Bistabiel", bistabielId); - if (QBridgeHandler == null) { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Bistabiel", bistabielId); + if (qBridgeHandler == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No communication with Qbus Bridge!"); return; @@ -72,13 +72,13 @@ public void initialize() { setSN(); - Map bistabielComm = QComm.getBistabiel(); + Map bistabielComm = qComm.getBistabiel(); if (bistabielComm != null) { - QbusBistabiel QBistabiel = bistabielComm.get(bistabielId); - if (QBistabiel != null) { - QBistabiel.setThingHandler(this); - handleStateUpdate(QBistabiel); + QbusBistabiel qBistabiel = bistabielComm.get(bistabielId); + if (qBistabiel != null) { + qBistabiel.setThingHandler(this); + handleStateUpdate(qBistabiel); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Error while initializing the thing."); @@ -102,14 +102,13 @@ public void initialize() { * Sets the serial number of the controller */ public void setSN() { - QbusBridgeHandler QBridgeHandler = getBridgeHandler("Bistabiel", bistabielId); - if (QBridgeHandler == null) { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Bistabiel", bistabielId); + if (qBridgeHandler == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No communication with Qbus Bridge!"); return; - } else { - this.sn = QBridgeHandler.getSn(); } + this.sn = qBridgeHandler.getSn(); } /** @@ -117,36 +116,36 @@ public void setSN() { */ @Override public void handleCommand(ChannelUID channelUID, Command command) { - QbusCommunication QComm = getCommunication("Bistabiel", bistabielId); - if (QComm == null) { + QbusCommunication qComm = getCommunication("Bistabiel", bistabielId); + if (qComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge communication not initialized when trying to execute command for bistabiel " + bistabielId); return; } - Map bistabielComm = QComm.getBistabiel(); + Map bistabielComm = qComm.getBistabiel(); if (bistabielComm != null) { - QbusBistabiel QBistabiel = bistabielComm.get(bistabielId); + QbusBistabiel qBistabiel = bistabielComm.get(bistabielId); - if (QBistabiel == null) { + if (qBistabiel == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "bridge communication not initialized when trying to execute command for bistabiel " + bistabielId); return; } else { scheduler.submit(() -> { - if (!QComm.communicationActive()) { - restartCommunication(QComm, "Bistabiel", bistabielId); + if (!qComm.communicationActive()) { + restartCommunication(qComm, "Bistabiel", bistabielId); } - if (QComm.communicationActive()) { + if (qComm.communicationActive()) { if (command == REFRESH) { - handleStateUpdate(QBistabiel); + handleStateUpdate(qBistabiel); return; } - handleSwitchCommand(QBistabiel, channelUID, command); + handleSwitchCommand(qBistabiel, channelUID, command); } }); } @@ -161,15 +160,15 @@ public void handleCommand(ChannelUID channelUID, Command command) { * * @throws InterruptedException */ - private void handleSwitchCommand(QbusBistabiel QBistabiel, ChannelUID channelUID, Command command) { + private void handleSwitchCommand(QbusBistabiel qBistabiel, ChannelUID channelUID, Command command) { if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; String snr = getSN(); if (snr != null) { if (s == OnOffType.OFF) { - QBistabiel.execute(0, snr); + qBistabiel.execute(0, snr); } else { - QBistabiel.execute(100, snr); + qBistabiel.execute(100, snr); } } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, @@ -181,8 +180,8 @@ private void handleSwitchCommand(QbusBistabiel QBistabiel, ChannelUID channelUID /** * Method to update state of channel, called from Qbus Bistabiel. */ - public void handleStateUpdate(QbusBistabiel QBistabiel) { - Integer bistabielState = QBistabiel.getState(); + public void handleStateUpdate(QbusBistabiel qBistabiel) { + Integer bistabielState = qBistabiel.getState(); if (bistabielState != null) { updateState(CHANNEL_SWITCH, (bistabielState == 0) ? OnOffType.OFF : OnOffType.ON); updateStatus(ThingStatus.ONLINE); diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java index 804b6b9608409..f901fb284ae76 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java @@ -38,7 +38,6 @@ @NonNullByDefault public class QbusCO2Handler extends QbusGlobalHandler { - protected @Nullable QbusThingsConfig config; private int co2Id; @@ -54,31 +53,30 @@ public QbusCO2Handler(Thing thing) { */ @Override public void initialize() { - setConfig(); co2Id = getId(); - QbusCommunication QComm = getCommunication("CO2", co2Id); - if (QComm == null) { + QbusCommunication qComm = getCommunication("CO2", co2Id); + if (qComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No communication with Qbus Bridge!"); return; } - QbusBridgeHandler QBridgeHandler = getBridgeHandler("CO2", co2Id); - if (QBridgeHandler == null) { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("CO2", co2Id); + if (qBridgeHandler == null) { return; } setSN(); - Map co2Comm = QComm.getCo2(); + Map co2Comm = qComm.getCo2(); if (co2Comm != null) { - QbusCO2 QCo2 = co2Comm.get(co2Id); - if (QCo2 != null) { - QCo2.setThingHandler(this); - handleStateUpdate(QCo2); + QbusCO2 qCo2 = co2Comm.get(co2Id); + if (qCo2 != null) { + qCo2.setThingHandler(this); + handleStateUpdate(qCo2); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Error while initializing the thing."); @@ -94,33 +92,34 @@ public void initialize() { */ @Override public void handleCommand(ChannelUID channelUID, Command command) { - QbusCommunication QComm = getCommunication("CO2", co2Id); - if (QComm == null) { + QbusCommunication qComm = getCommunication("CO2", co2Id); + if (qComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge communication not initialized when trying to execute command for CO2 " + co2Id); return; } - Map co2Comm = QComm.getCo2(); + Map co2Comm = qComm.getCo2(); if (co2Comm != null) { - QbusCO2 QCo2 = co2Comm.get(co2Id); - if (QCo2 == null) { + QbusCO2 qCo2 = co2Comm.get(co2Id); + if (qCo2 == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge communication not initialized when trying to execute command for CO2 " + co2Id); return; } else { scheduler.submit(() -> { - if (!QComm.communicationActive()) { - restartCommunication(QComm, "CO2", co2Id); + if (!qComm.communicationActive()) { + restartCommunication(qComm, "CO2", co2Id); } - if (QComm.communicationActive()) { + if (qComm.communicationActive()) { if (command == REFRESH) { - handleStateUpdate(QCo2); + handleStateUpdate(qCo2); return; } } + }); } } @@ -129,10 +128,10 @@ public void handleCommand(ChannelUID channelUID, Command command) { /** * Method to update state of channel, called from Qbus CO2. */ - public void handleStateUpdate(QbusCO2 QCo2) { - Integer CO2State = QCo2.getState(); - if (CO2State != null) { - updateState(CHANNEL_CO2, new DecimalType(CO2State)); + public void handleStateUpdate(QbusCO2 qCo2) { + Integer co2State = qCo2.getState(); + if (co2State != null) { + updateState(CHANNEL_CO2, new DecimalType(co2State)); updateStatus(ThingStatus.ONLINE); } } @@ -150,14 +149,13 @@ public void handleStateUpdate(QbusCO2 QCo2) { * Sets the serial number of the controller */ public void setSN() { - QbusBridgeHandler QBridgeHandler = getBridgeHandler("CO2", co2Id); - if (QBridgeHandler == null) { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("CO2", co2Id); + if (qBridgeHandler == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No communication with Qbus Bridge!"); return; - } else { - this.sn = QBridgeHandler.getSn(); } + this.sn = qBridgeHandler.getSn(); } /** diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java index c2d5281965be4..eb26fe7487db1 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java @@ -58,26 +58,26 @@ public void initialize() { setConfig(); dimmerId = getId(); - QbusCommunication QComm = getCommunication("Dimmer", dimmerId); - if (QComm == null) { + QbusCommunication qComm = getCommunication("Dimmer", dimmerId); + if (qComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No communication with Qbus Bridge!"); return; } - QbusBridgeHandler QBridgeHandler = getBridgeHandler("Dimmer", dimmerId); - if (QBridgeHandler == null) { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Dimmer", dimmerId); + if (qBridgeHandler == null) { return; } setSN(); - Map dimmerComm = QComm.getDimmer(); + Map dimmerComm = qComm.getDimmer(); if (dimmerComm != null) { - QbusDimmer QDimmer = dimmerComm.get(dimmerId); - if (QDimmer != null) { - QDimmer.setThingHandler(this); - handleStateUpdate(QDimmer); + QbusDimmer qDimmer = dimmerComm.get(dimmerId); + if (qDimmer != null) { + qDimmer.setThingHandler(this); + handleStateUpdate(qDimmer); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Error while initializing the thing."); @@ -101,14 +101,14 @@ public void initialize() { * Sets the serial number of the controller */ public void setSN() { - QbusBridgeHandler QBridgeHandler = getBridgeHandler("Dimmer", dimmerId); - if (QBridgeHandler == null) { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Dimmer", dimmerId); + if (qBridgeHandler == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No communication with Qbus Bridge!"); return; + } else { + this.sn = qBridgeHandler.getSn(); } - this.sn = QBridgeHandler.getSn(); - ; } /** @@ -116,41 +116,41 @@ public void setSN() { */ @Override public void handleCommand(ChannelUID channelUID, Command command) { - QbusCommunication QComm = getCommunication("Dimmer", dimmerId); + QbusCommunication qComm = getCommunication("Dimmer", dimmerId); - if (QComm == null) { + if (qComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge communication not initialized when trying to execute command for dimmer " + dimmerId); return; } - Map dimmerComm = QComm.getDimmer(); + Map dimmerComm = qComm.getDimmer(); if (dimmerComm != null) { - QbusDimmer QDimmer = dimmerComm.get(dimmerId); + QbusDimmer qDimmer = dimmerComm.get(dimmerId); - if (QDimmer == null) { + if (qDimmer == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge communication not initialized when trying to execute command for dimmer " + dimmerId); return; } else { scheduler.submit(() -> { - if (!QComm.communicationActive()) { - restartCommunication(QComm, "Dimmer", dimmerId); + if (!qComm.communicationActive()) { + restartCommunication(qComm, "Dimmer", dimmerId); } - if (QComm.communicationActive()) { + if (qComm.communicationActive()) { if (command == REFRESH) { - handleStateUpdate(QDimmer); + handleStateUpdate(qDimmer); return; } switch (channelUID.getId()) { case CHANNEL_SWITCH: - handleSwitchCommand(QDimmer, command); + handleSwitchCommand(qDimmer, command); break; case CHANNEL_BRIGHTNESS: - handleBrightnessCommand(QDimmer, command); + handleBrightnessCommand(qDimmer, command); break; } } @@ -173,20 +173,19 @@ public void thingOffline(String message) { /** * Executes the switch command */ - private void handleSwitchCommand(QbusDimmer QDimmer, Command command) { + private void handleSwitchCommand(QbusDimmer qDimmer, Command command) { if (command instanceof OnOffType) { - OnOffType s = (OnOffType) command; String snr = getSN(); - if (s == OnOffType.OFF) { + if (command == OnOffType.OFF) { if (snr != null) { - QDimmer.execute(0, snr); + qDimmer.execute(0, snr); } else { thingOffline("No serial number configured for " + dimmerId); } } else { if (snr != null) { - QDimmer.execute(1000, snr); + qDimmer.execute(1000, snr); } else { thingOffline("No serial number configured for " + dimmerId); } @@ -197,37 +196,35 @@ private void handleSwitchCommand(QbusDimmer QDimmer, Command command) { /** * Executes the brightness command */ - private void handleBrightnessCommand(QbusDimmer QDimmer, Command command) { + private void handleBrightnessCommand(QbusDimmer qDimmer, Command command) { String snr = getSN(); if (command instanceof OnOffType) { - OnOffType s = (OnOffType) command; - if (s == OnOffType.OFF) { + if (command == OnOffType.OFF) { if (snr != null) { - QDimmer.execute(0, snr); + qDimmer.execute(0, snr); } else { thingOffline("No serial number configured for " + dimmerId); } } else { if (snr != null) { - QDimmer.execute(100, snr); + qDimmer.execute(100, snr); } else { thingOffline("No serial number configured for " + dimmerId); } } } else if (command instanceof IncreaseDecreaseType) { - IncreaseDecreaseType s = (IncreaseDecreaseType) command; int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); - Integer currentValue = QDimmer.getState(); + Integer currentValue = qDimmer.getState(); Integer newValue; Integer sendvalue; if (currentValue != null) { - if (s == IncreaseDecreaseType.INCREASE) { + if (command == IncreaseDecreaseType.INCREASE) { newValue = currentValue + stepValue; // round down to step multiple newValue = newValue - newValue % stepValue; sendvalue = newValue > 100 ? 100 : newValue; if (snr != null) { - QDimmer.execute(sendvalue, snr); + qDimmer.execute(sendvalue, snr); } else { thingOffline("No serial number configured for " + dimmerId); } @@ -237,7 +234,7 @@ private void handleBrightnessCommand(QbusDimmer QDimmer, Command command) { newValue = newValue + newValue % stepValue; sendvalue = newValue < 0 ? 0 : newValue; if (snr != null) { - QDimmer.execute(sendvalue, snr); + qDimmer.execute(sendvalue, snr); } else { thingOffline("No serial number configured for " + dimmerId); } @@ -246,15 +243,15 @@ private void handleBrightnessCommand(QbusDimmer QDimmer, Command command) { } else if (command instanceof PercentType) { PercentType p = (PercentType) command; int pp = p.intValue(); - if (p == PercentType.ZERO) { + if (command == PercentType.ZERO) { if (snr != null) { - QDimmer.execute(0, snr); + qDimmer.execute(0, snr); } else { thingOffline("No serial number configured for " + dimmerId); } } else { if (snr != null) { - QDimmer.execute(pp, snr); + qDimmer.execute(pp, snr); } else { thingOffline("No serial number configured for " + dimmerId); } @@ -265,8 +262,8 @@ private void handleBrightnessCommand(QbusDimmer QDimmer, Command command) { /** * Method to update state of channel, called from Qbus Dimmer. */ - public void handleStateUpdate(QbusDimmer QDimmer) { - Integer dimmerState = QDimmer.getState(); + public void handleStateUpdate(QbusDimmer qDimmer) { + Integer dimmerState = qDimmer.getState(); if (dimmerState != null) { updateState(CHANNEL_BRIGHTNESS, new PercentType(dimmerState)); updateStatus(ThingStatus.ONLINE); diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java index f25448570ade2..072c1aecfc32f 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java @@ -43,14 +43,14 @@ public QbusGlobalHandler(Thing thing) { * @return */ public @Nullable QbusCommunication getCommunication(String type, int globalId) { - QbusBridgeHandler QBridgeHandler = getBridgeHandler(type, globalId); - if (QBridgeHandler == null) { + QbusBridgeHandler qBridgeHandler = getBridgeHandler(type, globalId); + if (qBridgeHandler == null) { updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED, "No bridge handler initialized for " + type + " with id " + globalId + "."); return null; } - QbusCommunication QComm = QBridgeHandler.getCommunication(); - return QComm; + QbusCommunication qComm = qBridgeHandler.getCommunication(); + return qComm; } /** @@ -61,33 +61,33 @@ public QbusGlobalHandler(Thing thing) { * @return */ public @Nullable QbusBridgeHandler getBridgeHandler(String type, int globalId) { - Bridge QBridge = getBridge(); - if (QBridge == null) { + Bridge qBridge = getBridge(); + if (qBridge == null) { updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED, "No bridge initialized for " + type + " with ID " + globalId); return null; } - QbusBridgeHandler QBridgeHandler = (QbusBridgeHandler) QBridge.getHandler(); - return QBridgeHandler; + QbusBridgeHandler qBridgeHandler = (QbusBridgeHandler) qBridge.getHandler(); + return qBridgeHandler; } /** * - * @param QComm + * @param qComm * @param type * @param globalId */ - public void restartCommunication(QbusCommunication QComm, String type, int globalId) { - QComm.restartCommunication(); + public void restartCommunication(QbusCommunication qComm, String type, int globalId) { + qComm.restartCommunication(); - if (!QComm.communicationActive()) { + if (!qComm.communicationActive()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Communication socket error"); return; } - QbusBridgeHandler QBridgeHandler = getBridgeHandler(type, globalId); - if (QBridgeHandler != null) { - QBridgeHandler.bridgeOnline(); + QbusBridgeHandler qBridgeHandler = getBridgeHandler(type, globalId); + if (qBridgeHandler != null) { + qBridgeHandler.bridgeOnline(); } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java index 530e4dc2772a4..391487e335a74 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java @@ -57,31 +57,30 @@ public QbusRolHandler(Thing thing) { */ @Override public void initialize() { - setConfig(); rolId = getId(); - QbusCommunication QComm = getCommunication("Screen/Store", rolId); - if (QComm == null) { + QbusCommunication qComm = getCommunication("Screen/Store", rolId); + if (qComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No communication with Qbus Bridge!"); return; } - QbusBridgeHandler QBridgeHandler = getBridgeHandler("Screen/Store", rolId); - if (QBridgeHandler == null) { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Screen/Store", rolId); + if (qBridgeHandler == null) { return; } setSN(); - Map rolComm = QComm.getRol(); + Map rolComm = qComm.getRol(); if (rolComm != null) { - QbusRol QRol = rolComm.get(rolId); - if (QRol != null) { - QRol.setThingHandler(this); - handleStateUpdate(QRol); + QbusRol qRol = rolComm.get(rolId); + if (qRol != null) { + qRol.setThingHandler(this); + handleStateUpdate(qRol); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Error while initializing the thing."); @@ -105,14 +104,13 @@ public void initialize() { * Sets the serial number of the controller */ public void setSN() { - QbusBridgeHandler QBridgeHandler = getBridgeHandler("Screen/Store", rolId); - if (QBridgeHandler == null) { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Screen/Store", rolId); + if (qBridgeHandler == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No communication with Qbus Bridge!"); return; } - this.sn = QBridgeHandler.getSn(); - ; + this.sn = qBridgeHandler.getSn(); } /** @@ -120,41 +118,41 @@ public void setSN() { */ @Override public void handleCommand(ChannelUID channelUID, Command command) { - QbusCommunication QComm = getCommunication("Screen/Store", rolId); + QbusCommunication qComm = getCommunication("Screen/Store", rolId); - if (QComm == null) { + if (qComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge communication not initialized when trying to execute command for Screen/Store " + rolId); return; } - Map rolComm = QComm.getRol(); + Map rolComm = qComm.getRol(); if (rolComm != null) { - QbusRol QRol = rolComm.get(rolId); - if (QRol == null) { + QbusRol qRol = rolComm.get(rolId); + if (qRol == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge communication not initialized when trying to execute command for ROL " + rolId); return; } else { scheduler.submit(() -> { - if (!QComm.communicationActive()) { - restartCommunication(QComm, "Screen/Store", rolId); + if (!qComm.communicationActive()) { + restartCommunication(qComm, "Screen/Store", rolId); } - if (QComm.communicationActive()) { + if (qComm.communicationActive()) { if (command == REFRESH) { - handleStateUpdate(QRol); + handleStateUpdate(qRol); return; } switch (channelUID.getId()) { case CHANNEL_ROLLERSHUTTER: - handleScreenposCommand(QRol, command); + handleScreenposCommand(qRol, command); break; case CHANNEL_SLATS: - handleSlatsposCommand(QRol, command); + handleSlatsposCommand(qRol, command); break; } } @@ -174,19 +172,19 @@ public void thingOffline(String message) { /** * Executes the command for screen up/down position */ - private void handleScreenposCommand(QbusRol QRol, Command command) { + private void handleScreenposCommand(QbusRol qRol, Command command) { String snr = getSN(); if (command instanceof UpDownType) { UpDownType upDown = (UpDownType) command; if (upDown == DOWN) { if (snr != null) { - QRol.execute(0, snr); + qRol.execute(0, snr); } else { thingOffline("No serial number configured for " + rolId); } } else { if (snr != null) { - QRol.execute(100, snr); + qRol.execute(100, snr); } else { thingOffline("No serial number configured for " + rolId); } @@ -194,7 +192,7 @@ private void handleScreenposCommand(QbusRol QRol, Command command) { } else if (command instanceof IncreaseDecreaseType) { IncreaseDecreaseType inc = (IncreaseDecreaseType) command; int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); - Integer currentValue = QRol.getState(); + Integer currentValue = qRol.getState(); int newValue; int sendValue; if (currentValue != null) { @@ -204,7 +202,7 @@ private void handleScreenposCommand(QbusRol QRol, Command command) { newValue = newValue - newValue % stepValue; sendValue = newValue > 100 ? 100 : newValue; if (snr != null) { - QRol.execute(sendValue, snr); + qRol.execute(sendValue, snr); } else { thingOffline("No serial number configured for " + rolId); } @@ -214,7 +212,7 @@ private void handleScreenposCommand(QbusRol QRol, Command command) { newValue = newValue + newValue % stepValue; sendValue = newValue > 100 ? 100 : newValue; if (snr != null) { - QRol.execute(sendValue, snr); + qRol.execute(sendValue, snr); } else { thingOffline("No serial number configured for " + rolId); } @@ -225,13 +223,13 @@ private void handleScreenposCommand(QbusRol QRol, Command command) { int pp = p.intValue(); if (p == PercentType.ZERO) { if (snr != null) { - QRol.execute(0, snr); + qRol.execute(0, snr); } else { thingOffline("No serial number configured for " + rolId); } } else { if (snr != null) { - QRol.execute(pp, snr); + qRol.execute(pp, snr); } else { thingOffline("No serial number configured for " + rolId); } @@ -242,19 +240,19 @@ private void handleScreenposCommand(QbusRol QRol, Command command) { /** * Executes the command for screen slats position */ - private void handleSlatsposCommand(QbusRol QRol, Command command) { + private void handleSlatsposCommand(QbusRol qRol, Command command) { String snr = getSN(); if (command instanceof org.openhab.core.library.types.UpDownType) { org.openhab.core.library.types.UpDownType s = (org.openhab.core.library.types.UpDownType) command; if (s == org.openhab.core.library.types.UpDownType.DOWN) { if (snr != null) { - QRol.executeSlats(0, snr); + qRol.executeSlats(0, snr); } else { thingOffline("No serial number configured for " + rolId); } } else { if (snr != null) { - QRol.executeSlats(100, snr); + qRol.executeSlats(100, snr); } else { thingOffline("No serial number configured for " + rolId); } @@ -262,7 +260,7 @@ private void handleSlatsposCommand(QbusRol QRol, Command command) { } else if (command instanceof IncreaseDecreaseType) { IncreaseDecreaseType s = (IncreaseDecreaseType) command; int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); - Integer currentValue = QRol.getState(); + Integer currentValue = qRol.getState(); int newValue; int sendValue; if (currentValue != null) { @@ -272,7 +270,7 @@ private void handleSlatsposCommand(QbusRol QRol, Command command) { newValue = newValue - newValue % stepValue; sendValue = newValue > 100 ? 100 : newValue; if (snr != null) { - QRol.executeSlats(sendValue, snr); + qRol.executeSlats(sendValue, snr); } else { thingOffline("No serial number configured for " + rolId); } @@ -282,7 +280,7 @@ private void handleSlatsposCommand(QbusRol QRol, Command command) { newValue = newValue + newValue % stepValue; sendValue = newValue > 100 ? 100 : newValue; if (snr != null) { - QRol.executeSlats(sendValue, snr); + qRol.executeSlats(sendValue, snr); } else { thingOffline("No serial number configured for " + rolId); } @@ -293,13 +291,13 @@ private void handleSlatsposCommand(QbusRol QRol, Command command) { int pp = p.intValue(); if (p == PercentType.ZERO) { if (snr != null) { - QRol.executeSlats(0, snr); + qRol.executeSlats(0, snr); } else { thingOffline("No serial number configured for " + rolId); } } else { if (snr != null) { - QRol.executeSlats(pp, snr); + qRol.executeSlats(pp, snr); } else { thingOffline("No serial number configured for " + rolId); } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java index 71d3973b708de..f445ec18a6734 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java @@ -53,30 +53,29 @@ public QbusSceneHandler(Thing thing) { */ @Override public void initialize() { - setConfig(); sceneId = getId(); - QbusCommunication QComm = getCommunication("Scene", sceneId); - if (QComm == null) { + QbusCommunication qComm = getCommunication("Scene", sceneId); + if (qComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No communication with Qbus Bridge!"); return; } - QbusBridgeHandler QBridgeHandler = getBridgeHandler("Scene", sceneId); - if (QBridgeHandler == null) { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Scene", sceneId); + if (qBridgeHandler == null) { return; } setSN(); - Map sceneComm = QComm.getScene(); + Map sceneComm = qComm.getScene(); if (sceneComm != null) { - QbusScene QScene = sceneComm.get(sceneId); - if (QScene != null) { - QScene.setThingHandler(this); + QbusScene qScene = sceneComm.get(sceneId); + if (qScene != null) { + qScene.setThingHandler(this); updateStatus(ThingStatus.ONLINE); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, @@ -101,14 +100,13 @@ public void initialize() { * Sets the serial number of the controller */ public void setSN() { - QbusBridgeHandler QBridgeHandler = getBridgeHandler("Scene", sceneId); - if (QBridgeHandler == null) { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Scene", sceneId); + if (qBridgeHandler == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No communication with Qbus Bridge!"); return; } - this.sn = QBridgeHandler.getSn(); - ; + this.sn = qBridgeHandler.getSn(); } /** @@ -116,32 +114,32 @@ public void setSN() { */ @Override public void handleCommand(ChannelUID channelUID, Command command) { - QbusCommunication QComm = getCommunication("Scene", sceneId); + QbusCommunication qComm = getCommunication("Scene", sceneId); - if (QComm == null) { + if (qComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge communication not initialized when trying to execute command for Scene " + sceneId); return; } - Map sceneComm = QComm.getScene(); + Map sceneComm = qComm.getScene(); if (sceneComm != null) { - QbusScene QScene = sceneComm.get(sceneId); - if (QScene == null) { + QbusScene qScene = sceneComm.get(sceneId); + if (qScene == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge communication not initialized when trying to execute command for Scene " + sceneId); return; } else { scheduler.submit(() -> { - if (!QComm.communicationActive()) { - restartCommunication(QComm, "Scene", sceneId); + if (!qComm.communicationActive()) { + restartCommunication(qComm, "Scene", sceneId); } - if (QComm.communicationActive()) { + if (qComm.communicationActive()) { switch (channelUID.getId()) { case CHANNEL_SCENE: - handleSwitchCommand(QScene, channelUID, command); + handleSwitchCommand(qScene, channelUID, command); break; } } @@ -153,8 +151,8 @@ public void handleCommand(ChannelUID channelUID, Command command) { /** * Method to update state of channel, called from Qbus Scene. */ - public void handleStateUpdate(QbusScene QScene) { - Integer sceneState = QScene.getState(); + public void handleStateUpdate(QbusScene qScene) { + Integer sceneState = qScene.getState(); if (sceneState != null) { updateState(CHANNEL_SCENE, (sceneState == 0) ? OnOffType.OFF : OnOffType.ON); updateStatus(ThingStatus.ONLINE); @@ -172,19 +170,19 @@ public void thingOffline(String message) { /** * Executes the scene command */ - private void handleSwitchCommand(QbusScene QScene, ChannelUID channelUID, Command command) { + private void handleSwitchCommand(QbusScene qScene, ChannelUID channelUID, Command command) { String snr = getSN(); if (command instanceof OnOffType) { OnOffType s = (OnOffType) command; if (s == OnOffType.OFF) { if (snr != null) { - QScene.execute(0, snr); + qScene.execute(0, snr); } else { thingOffline("No serial number configured for " + sceneId); } } else { if (snr != null) { - QScene.execute(100, snr); + qScene.execute(100, snr); } else { thingOffline("No serial number configured for " + sceneId); } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java index 1797f2a8c0fb9..7ebf38c9b4459 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java @@ -59,27 +59,27 @@ public void initialize() { setConfig(); thermostatId = getId(); - QbusCommunication QComm = getCommunication("Thermostat", thermostatId); - if (QComm == null) { + QbusCommunication qComm = getCommunication("Thermostat", thermostatId); + if (qComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No communication with Qbus Bridge!"); return; } - QbusBridgeHandler QBridgeHandler = getBridgeHandler("Thermostat", thermostatId); - if (QBridgeHandler == null) { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Thermostat", thermostatId); + if (qBridgeHandler == null) { return; } setSN(); - Map thermostatComm = QComm.getThermostat(); + Map thermostatComm = qComm.getThermostat(); if (thermostatComm != null) { - QbusThermostat QThermostat = thermostatComm.get(thermostatId); - if (QThermostat != null) { - QThermostat.setThingHandler(this); - handleStateUpdate(QThermostat); + QbusThermostat qThermostat = thermostatComm.get(thermostatId); + if (qThermostat != null) { + qThermostat.setThingHandler(this); + handleStateUpdate(qThermostat); } else { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "Error while initializing the thing."); @@ -103,14 +103,13 @@ public void initialize() { * Sets the serial number of the controller */ public void setSN() { - QbusBridgeHandler QBridgeHandler = getBridgeHandler("Thermostat", thermostatId); - if (QBridgeHandler == null) { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Thermostat", thermostatId); + if (qBridgeHandler == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, "No communication with Qbus Bridge!"); return; - } else { - this.sn = QBridgeHandler.getSn(); } + this.sn = qBridgeHandler.getSn(); } /** @@ -118,45 +117,44 @@ public void setSN() { */ @Override public void handleCommand(ChannelUID channelUID, Command command) { - QbusCommunication QComm = getCommunication("Thermostat", thermostatId); + QbusCommunication qComm = getCommunication("Thermostat", thermostatId); - if (QComm == null) { + if (qComm == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge communication not initialized when trying to execute command for thermostat " + thermostatId); return; } - Map thermostatComm = QComm.getThermostat(); + Map thermostatComm = qComm.getThermostat(); if (thermostatComm != null) { + QbusThermostat qThermostat = thermostatComm.get(thermostatId); - QbusThermostat QThermostat = thermostatComm.get(thermostatId); - - if (QThermostat == null) { + if (qThermostat == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "Bridge communication not initialized when trying to execute command for Scene " + thermostatId); return; } else { scheduler.submit(() -> { - if (!QComm.communicationActive()) { - restartCommunication(QComm, "Thermostat", thermostatId); + if (!qComm.communicationActive()) { + restartCommunication(qComm, "Thermostat", thermostatId); } - if (QComm.communicationActive()) { + if (qComm.communicationActive()) { if (command == REFRESH) { - handleStateUpdate(QThermostat); + handleStateUpdate(qThermostat); return; } switch (channelUID.getId()) { case CHANNEL_MODE: - handleModeCommand(QThermostat, command); + handleModeCommand(qThermostat, command); break; case CHANNEL_SETPOINT: - handleSetpointCommand(QThermostat, command); + handleSetpointCommand(qThermostat, command); break; } @@ -178,12 +176,12 @@ public void thingOffline(String message) { /** * Executes the Mode command */ - private void handleModeCommand(QbusThermostat QThermostat, Command command) { + private void handleModeCommand(QbusThermostat qThermostat, Command command) { String snr = getSN(); if (command instanceof DecimalType) { int mode = ((DecimalType) command).intValue(); if (snr != null) { - QThermostat.executeMode(mode, snr); + qThermostat.executeMode(mode, snr); } else { thingOffline("No serial number configured for " + thermostatId); } @@ -193,13 +191,14 @@ private void handleModeCommand(QbusThermostat QThermostat, Command command) { /** * Executes the Setpoint command */ - private void handleSetpointCommand(QbusThermostat QThermostat, Command command) { + private void handleSetpointCommand(QbusThermostat qThermostat, Command command) { String snr = getSN(); if (command instanceof QuantityType) { QuantityType s = (QuantityType) command; double sp = s.doubleValue(); + s.toUnit(CELSIUS); if (snr != null) { - QThermostat.executeSetpoint(sp, snr); + qThermostat.executeSetpoint(sp, snr); } else { thingOffline("No serial number configured for " + thermostatId); } @@ -209,15 +208,15 @@ private void handleSetpointCommand(QbusThermostat QThermostat, Command command) /** * Method to update state of all channels, called from Qbus thermostat. * - * @param QThermostat Qbus thermostat + * @param qThermostat Qbus thermostat * */ - public void handleStateUpdate(QbusThermostat QThermostat) { - updateState(CHANNEL_MEASURED, new QuantityType<>(QThermostat.getMeasured(), CELSIUS)); + public void handleStateUpdate(QbusThermostat qThermostat) { + updateState(CHANNEL_MEASURED, new QuantityType<>(qThermostat.getMeasured(), CELSIUS)); - updateState(CHANNEL_SETPOINT, new QuantityType<>(QThermostat.getSetpoint(), CELSIUS)); + updateState(CHANNEL_SETPOINT, new QuantityType<>(qThermostat.getSetpoint(), CELSIUS)); - updateState(CHANNEL_MODE, new DecimalType(QThermostat.getMode())); + updateState(CHANNEL_MODE, new DecimalType(qThermostat.getMode())); updateStatus(ThingStatus.ONLINE); } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java index 4e2b3b9ba70f5..978a4672d16bc 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java @@ -29,7 +29,7 @@ public final class QbusBistabiel { private final Logger logger = LoggerFactory.getLogger(QbusBistabiel.class); - private @Nullable QbusCommunication QComm; + private @Nullable QbusCommunication qComm; private String id; @@ -53,14 +53,14 @@ public void setThingHandler(QbusBistabielHandler handler) { } /** - * This method sets a pointer to the QComm BISTABIEL of class {@link QbusCommuncation}. + * This method sets a pointer to the qComm BISTABIEL of class {@link QbusCommuncation}. * This is then used to be able to call back the sendCommand method in this class to send a command to the * Qbus IP-interface when.. * - * @param QComm + * @param qComm */ - public void setQComm(QbusCommunication QComm) { - this.QComm = QComm; + public void setQComm(QbusCommunication qComm) { + this.qComm = qComm; } /** @@ -95,13 +95,14 @@ void setState(int state) { * @throws InterruptedException */ public void execute(int value, String sn) { - QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeBistabiel").withId(this.id).withState(value); - QbusCommunication comm = QComm; + QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeBistabiel").withId(this.id).withState(value); + QbusCommunication comm = qComm; if (comm != null) { try { - comm.sendMessage(QCmd); + comm.sendMessage(qCmd); } catch (InterruptedException e) { - logger.warn("Could not send command for bistabiel {}", this.id); + logger.warn("Could not send command for bistabiel {}, {}", this.id, e.getMessage()); + } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java index be90cca920adb..abf59290aa28c 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java @@ -21,11 +21,14 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.QbusBridgeHandler; +import org.openhab.core.common.NamedThreadFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -63,9 +66,9 @@ public final class QbusCommunication { private Gson gsonOut = new Gson(); private Gson gsonIn; - private @Nullable String CTD; + private @Nullable String ctd; - private boolean CTDConnected = false; + private boolean ctdConnected = false; private final Map bistabiel = new HashMap<>(); private final Map scene = new HashMap<>(); @@ -74,6 +77,9 @@ public final class QbusCommunication { private final Map thermostat = new HashMap<>(); private final Map co2 = new HashMap<>(); + private final ExecutorService threadExecutor = Executors + .newSingleThreadExecutor(new NamedThreadFactory("org.openhab.binding.qbus", true)); + private @Nullable QbusBridgeHandler bridgeCallBack; /** @@ -100,7 +106,7 @@ public QbusCommunication() { public synchronized void startCommunication() throws IOException, InterruptedException { QbusBridgeHandler handler = bridgeCallBack; - CTDConnected = false; + ctdConnected = false; if (qEventsRunning) { throw new IOException("Previous listening thread is still active."); @@ -128,16 +134,19 @@ public synchronized void startCommunication() throws IOException, InterruptedExc getSN(); // Connect to Qbus server - Connect(); + connect(); // If Qbus Client is connected then initialize and start listening for incoming commands, else put Bridge // offline - if (CTDConnected == true) { + if (ctdConnected) { initialize(); - Thread thread = new Thread(this::qEvents); - thread.setName("OH-binding"); - thread.setDaemon(true); - thread.start(); + threadExecutor.execute(() -> { + try { + qEvents(); + } catch (IOException e) { + logger.debug("Could not start thread {} ", e.getMessage()); + } + }); } else { handler.bridgeOffline("No communication with Qbus client"); } @@ -163,8 +172,18 @@ public synchronized void stopCommunication() { } qSocket = null; + if (qOut != null) { + qOut.close(); + } - CTDConnected = false; + try { + if (qIn != null) { + qIn.close(); + } + } catch (IOException e) { + logger.debug("Error on closing reader {} ", e.getMessage()); + } + ctdConnected = false; logger.debug("Communication stopped from thread {}", Thread.currentThread().getId()); } @@ -181,7 +200,6 @@ public synchronized void restartCommunication() { try { startCommunication(); } catch (InterruptedException | IOException e) { - logger.warn("Error on restaring communication."); } } @@ -201,7 +219,7 @@ public boolean communicationActive() { */ public boolean clientConnected() { - return CTDConnected; + return ctdConnected; } /** @@ -212,9 +230,10 @@ public boolean clientConnected() { * Qbus outputs. It is started after initialization of the communication. * * @return + * @throws IOException * */ - private void qEvents() { + private void qEvents() throws IOException { String qMessage; logger.info("Listening for events on thread {}", Thread.currentThread().getId()); @@ -223,26 +242,22 @@ private void qEvents() { BufferedReader reader = qIn; - try { - if (reader == null) { - throw new IOException("Bufferreader for incomming messages not initialized."); - } - while (!Thread.currentThread().isInterrupted() & ((qMessage = reader.readLine()) != null)) { - if (qMessage != null) { - readMessage(qMessage); - } - } - } catch (IOException e) { - if (!listenerStopped) { - qEventsRunning = false; - logger.error("Qbus: IO error in listener on thread {}", Thread.currentThread().getId()); + if (reader == null) { + throw new IOException("Bufferreader for incomming messages not initialized."); + } + while (!Thread.currentThread().isInterrupted() && ((qMessage = reader.readLine()) != null)) { + readMessage(qMessage); + } - QbusBridgeHandler handler = bridgeCallBack; + if (!listenerStopped) { + qEventsRunning = false; + logger.debug("Qbus: IO error in listener on thread {}", Thread.currentThread().getId()); - if (handler != null) { - CTDConnected = false; - handler.bridgeOffline("No communication with Qbus server"); - } + QbusBridgeHandler handler = bridgeCallBack; + + if (handler != null) { + ctdConnected = false; + handler.bridgeOffline("No communication with Qbus server"); } } @@ -289,7 +304,6 @@ synchronized void sendMessage(Object qMessage) throws InterruptedException { * Called by other methods to Qbus server and read response */ private void sendAndReadMessage(String command) throws IOException, InterruptedException { - @Nullable String snr = getSN(); if (snr != null) { QbusMessageCmd qCmd = new QbusMessageCmd(snr, command); @@ -316,13 +330,13 @@ private void sendAndReadMessage(String command) throws IOException, InterruptedE */ private void readMessage(String qMessage) { String cmd = ""; - String CTD = ""; + String ctd = ""; String sn = null; QbusMessageBase qMessageGson; qMessageGson = gsonIn.fromJson(qMessage, QbusMessageBase.class); if (qMessageGson != null) { - CTD = qMessageGson.getSn(); + ctd = qMessageGson.getSn(); cmd = qMessageGson.getCmd(); } @@ -330,9 +344,9 @@ private void readMessage(String qMessage) { sn = bridgeCallBack.getSn(); } - if (sn != null && CTD != null) { + if (sn != null && ctd != null) { try { - if (Integer.parseInt(sn) == Integer.parseInt(CTD) && qMessageGson != null) { + if (Integer.parseInt(sn) == Integer.parseInt(ctd) && qMessageGson != null) { // Get the compatible outputs from the Qbus server if ("returnBistabiel".equals(cmd)) { cmdListBistabiel(((QbusMessageListMap) qMessageGson).getOutputs()); @@ -341,20 +355,20 @@ private void readMessage(String qMessage) { } else if (("returnThermostat").equals(cmd)) { cmdListThermostat(((QbusMessageListMap) qMessageGson).getOutputs()); } else if (("returnScene").equals(cmd)) { - cmdListscenes(((QbusMessageListMap) qMessageGson).getOutputs()); + cmdListScenes(((QbusMessageListMap) qMessageGson).getOutputs()); } else if (("returnCo2").equals(cmd)) { - cmdlistco2(((QbusMessageListMap) qMessageGson).getOutputs()); + cmdListCo2(((QbusMessageListMap) qMessageGson).getOutputs()); } else if (("returnRol02p").equals(cmd)) { - cmdlistrol(((QbusMessageListMap) qMessageGson).getOutputs()); + cmdListRol(((QbusMessageListMap) qMessageGson).getOutputs()); } else if (("returnSlat").equals(cmd)) { - cmdlistrolslats(((QbusMessageListMap) qMessageGson).getOutputs()); + cmdListRolSlats(((QbusMessageListMap) qMessageGson).getOutputs()); } // Incoming commands from Qbus Client to openHAB (event) else if ("updateBistabiel".equals(cmd)) { updateBistabiel(((QbusMessageListMap) qMessageGson).getOutputs()); } else if ("updateDimmer".equals(cmd)) { - updateDimmers(((QbusMessageListMap) qMessageGson).getOutputs()); + updateDimmer(((QbusMessageListMap) qMessageGson).getOutputs()); } else if ("updateThermostat".equals(cmd)) { updateThermostat(((QbusMessageListMap) qMessageGson).getOutputs()); } else if ("updateCo2".equals(cmd)) { @@ -362,7 +376,7 @@ else if ("updateBistabiel".equals(cmd)) { } else if ("updateRol02p".equals(cmd)) { updateRol(((QbusMessageListMap) qMessageGson).getOutputs()); } else if ("updateRol02pSlat".equals(cmd)) { - updateRolslats(((QbusMessageListMap) qMessageGson).getOutputs()); + updateRolSlats(((QbusMessageListMap) qMessageGson).getOutputs()); } // Incomming commands from Qbus server to verify the client connection @@ -399,7 +413,7 @@ else if ("disconnect".equals(cmd)) { private void initialize() throws IOException, InterruptedException { if (bridgeCallBack != null) { - if (CTDConnected) { + if (ctdConnected) { sendAndReadMessage("getBistabiel"); sendAndReadMessage("getScene"); sendAndReadMessage("getDimmer"); @@ -408,7 +422,7 @@ private void initialize() throws IOException, InterruptedException { sendAndReadMessage("getThermostat"); sendAndReadMessage("getCo2"); } else { - CTDConnected = false; + ctdConnected = false; QbusBridgeHandler handler = bridgeCallBack; @@ -424,13 +438,13 @@ private void initialize() throws IOException, InterruptedException { } public @Nullable String getSN() { - return this.CTD; + return this.ctd; } public void setSN() { - QbusBridgeHandler QBridgeHandler = bridgeCallBack; - if (QBridgeHandler != null) { - this.CTD = QBridgeHandler.getSn(); + QbusBridgeHandler qBridgeHandler = bridgeCallBack; + if (qBridgeHandler != null) { + this.ctd = qBridgeHandler.getSn(); } } @@ -440,14 +454,13 @@ public void setSN() { * @throws IOException * */ - private void Connect() throws InterruptedException, IOException { - @Nullable + private void connect() throws InterruptedException, IOException { String snr = getSN(); if (snr != null) { - QbusMessageCmd QCmd = new QbusMessageCmd(snr, "openHAB"); + QbusMessageCmd qCmd = new QbusMessageCmd(snr, "openHAB"); - sendMessage(QCmd); + sendMessage(qCmd); BufferedReader reader = qIn; if (reader == null) { @@ -487,7 +500,7 @@ private void cmdListBistabiel(@Nullable List> outputs) { qBistabiel.setState(state); } } else { - logger.error("Error in json for BistabBistabiel/Timers/Monos/Intervals"); + logger.debug("Error in json for BistabBistabiel/Timers/Monos/Intervals"); } } } @@ -498,17 +511,17 @@ private void cmdListBistabiel(@Nullable List> outputs) { * * @param outputs */ - private void cmdListscenes(@Nullable List> outputs) { + private void cmdListScenes(@Nullable List> outputs) { if (outputs != null) { for (Map scene : outputs) { String idStr = scene.get("id"); if (idStr != null) { int id = Integer.parseInt(idStr); - QbusScene Scene = new QbusScene(idStr); - Scene.setQComm(this); - this.scene.put(id, Scene); + QbusScene qScene = new QbusScene(idStr); + qScene.setQComm(this); + this.scene.put(id, qScene); } else { - logger.error("Error in json for Scenes"); + logger.debug("Error in json for Scenes"); } } } @@ -536,7 +549,7 @@ private void cmdListDimmers(@Nullable List> outputs) { qDimmer.updateState(state); } } else { - logger.error("Error in json for Dimmer"); + logger.debug("Error in json for Dimmer"); } } } @@ -547,7 +560,7 @@ private void cmdListDimmers(@Nullable List> outputs) { * * @param data */ - private void cmdlistrol(@Nullable List> outputs) { + private void cmdListRol(@Nullable List> outputs) { if (outputs != null) { for (Map rol : outputs) { String idStr = rol.get("id"); @@ -555,16 +568,16 @@ private void cmdlistrol(@Nullable List> outputs) { if (idStr != null && stateStr != null) { int id = Integer.parseInt(idStr); Integer rolpos = Integer.valueOf(stateStr); - QbusRol Qrol = new QbusRol(idStr); + QbusRol qRol = new QbusRol(idStr); if (!this.rol.containsKey(id)) { - Qrol.setQComm(this); - this.rol.put(id, Qrol); - Qrol.setState(rolpos); + qRol.setQComm(this); + this.rol.put(id, qRol); + qRol.setState(rolpos); } else { - Qrol.setState(rolpos); + qRol.setState(rolpos); } } else { - logger.error("Error in json for ROL02P"); + logger.debug("Error in json for ROL02P"); } } } @@ -575,7 +588,7 @@ private void cmdlistrol(@Nullable List> outputs) { * * @param data */ - private void cmdlistrolslats(@Nullable List> outputs) { + private void cmdListRolSlats(@Nullable List> outputs) { if (outputs != null) { for (Map rol : outputs) { String idStr = rol.get("id"); @@ -585,18 +598,18 @@ private void cmdlistrolslats(@Nullable List> outputs) { int id = Integer.parseInt(idStr); Integer rolpos = Integer.parseInt(rolPos); Integer rolposslats = Integer.parseInt(slatPos); - QbusRol Qrol = new QbusRol(idStr); + QbusRol qRol = new QbusRol(idStr); if (!this.rol.containsKey(id)) { - Qrol.setQComm(this); - this.rol.put(id, Qrol); - Qrol.setState(rolpos); - Qrol.setSlats(rolposslats); + qRol.setQComm(this); + this.rol.put(id, qRol); + qRol.setState(rolpos); + qRol.setSlats(rolposslats); } else { - Qrol.setState(rolpos); - Qrol.setSlats(rolposslats); + qRol.setState(rolpos); + qRol.setSlats(rolposslats); } } else { - logger.error("Error in json for ROL02P_Slats"); + logger.debug("Error in json for ROL02P_Slats"); } } } @@ -607,7 +620,7 @@ private void cmdlistrolslats(@Nullable List> outputs) { * * @param data */ - private void cmdlistco2(@Nullable List> outputs) { + private void cmdListCo2(@Nullable List> outputs) { if (outputs != null) { for (Map co2 : outputs) { String idStr = co2.get("id"); @@ -615,15 +628,15 @@ private void cmdlistco2(@Nullable List> outputs) { if (idStr != null && stateStr != null) { int id = Integer.parseInt(idStr); int state = Integer.parseInt(stateStr); - QbusCO2 CO2 = new QbusCO2(); + QbusCO2 qCO2 = new QbusCO2(); if (!this.co2.containsKey(id)) { - this.co2.put(id, CO2); - CO2.setState(state); + this.co2.put(id, qCO2); + qCO2.setState(state); } else { - CO2.setState(state); + qCO2.setState(state); } } else { - logger.error("Error in json for CO2"); + logger.debug("Error in json for CO2"); } } @@ -656,7 +669,7 @@ private void cmdListThermostat(@Nullable List> outputs) { qThermostat.updateState(measured, setpoint, mode); } } else { - logger.error("Error in json for Thermostats"); + logger.debug("Error in json for Thermostats"); } } } @@ -673,15 +686,15 @@ private void updateBistabiel(List> output) { String stateStr = bistabiel.get("state"); if (idStr != null && stateStr != null) { int id = Integer.parseInt(idStr); - int value1 = Integer.parseInt(stateStr); - QbusBistabiel Bistabiel = this.bistabiel.get(id); - if (Bistabiel != null) { + int state = Integer.parseInt(stateStr); + QbusBistabiel qBistabiel = this.bistabiel.get(id); + if (qBistabiel != null) { if (!this.bistabiel.containsKey(id)) { logger.warn("Bistabiel in controller not known {}", id); return; } - logger.info("Event execute bistabiel {} with state {}", id, value1); - Bistabiel.setState(value1); + logger.debug("Event execute bistabiel {} with state {}", id, state); + qBistabiel.setState(state); } } } @@ -692,21 +705,21 @@ private void updateBistabiel(List> output) { * * @param data */ - private void updateDimmers(List> data) { + private void updateDimmer(List> data) { for (Map dimmer : data) { String idStr = dimmer.get("id"); String stateStr = dimmer.get("state"); if (idStr != null && stateStr != null) { int id = Integer.valueOf(idStr); int value = Integer.valueOf(stateStr); - QbusDimmer Qdimmer = this.dimmer.get(id); + QbusDimmer qDimmer = this.dimmer.get(id); if (!this.dimmer.containsKey(id)) { logger.warn("Dimmer in controller not known {}", id); return; } - if (Qdimmer != null) { - logger.info("Event execute dimmer {} with state {}", id, value); - Qdimmer.setState(value); + if (qDimmer != null) { + logger.debug("Event execute dimmer {} with state {}", id, value); + qDimmer.setState(value); } } } @@ -724,14 +737,14 @@ private void updateRol(List> data) { if (idStr != null && posStr != null) { int id = Integer.valueOf(idStr); int pos = Integer.valueOf(posStr); - QbusRol Qrol = this.rol.get(id); + QbusRol qRol = this.rol.get(id); if (!this.rol.containsKey(id)) { logger.warn("Rol02p in controller not known {}", id); return; } - if (Qrol != null) { - logger.info("Event execute Rol02P {} with pos {}", id, pos); - Qrol.setState(pos); + if (qRol != null) { + logger.debug("Event execute Rol02P {} with pos {}", id, pos); + qRol.setState(pos); } } @@ -743,7 +756,7 @@ private void updateRol(List> data) { * * @param data */ - private void updateRolslats(List> data) { + private void updateRolSlats(List> data) { for (Map rol : data) { String idStr = rol.get("id"); String posStr = rol.get("pos"); @@ -752,15 +765,15 @@ private void updateRolslats(List> data) { int id = Integer.valueOf(idStr); int pos = Integer.valueOf(posStr); int slats = Integer.valueOf(slatsStr); - QbusRol Qrol = this.rol.get(id); + QbusRol qRol = this.rol.get(id); if (!this.rol.containsKey(id)) { logger.warn("Rol02p in controller not known {}", id); return; } - if (Qrol != null) { - logger.info("Event execute ROL02P_Slats {} with pos {} and slats {}", id, pos, slats); - Qrol.setState(pos); - Qrol.setSlats(slats); + if (qRol != null) { + logger.debug("Event execute ROL02P_Slats {} with pos {} and slats {}", id, pos, slats); + qRol.setState(pos); + qRol.setSlats(slats); } } } @@ -782,15 +795,15 @@ private void updateThermostat(List> data) { Double measured = Double.valueOf(measuredStr); Double setpoint = Double.valueOf(setpointdStr); Integer mode = Integer.valueOf(modedStr); - QbusThermostat Qthermostat = this.thermostat.get(id); + QbusThermostat qThermostat = this.thermostat.get(id); if (!this.thermostat.containsKey(id)) { logger.warn("Thermostat in controller not known {}", id); return; } - if (Qthermostat != null) { - logger.info("Event execute thermostat {} with measured {}, setpoint {}, mode {}", id, measured, + if (qThermostat != null) { + logger.debug("Event execute thermostat {} with measured {}, setpoint {}, mode {}", id, measured, setpoint, mode); - Qthermostat.updateState(measured, setpoint, mode); + qThermostat.updateState(measured, setpoint, mode); } } } @@ -808,14 +821,14 @@ private void updateCO2(List> data) { if (idStr != null && value1Str != null) { int id = Integer.valueOf(idStr); int value1 = Integer.valueOf(value1Str); - QbusCO2 Qco2 = this.co2.get(id); + QbusCO2 qCO2 = this.co2.get(id); if (!this.co2.containsKey(id)) { logger.warn("Co2 in controller not known {}", id); return; } - if (Qco2 != null) { - logger.info("Event execute co2 {} with state {}", id, value1); - Qco2.setState(value1); + if (qCO2 != null) { + logger.debug("Event execute co2 {} with state {}", id, value1); + qCO2.setState(value1); } } } @@ -825,7 +838,7 @@ private void updateCO2(List> data) { * Put Bridge offline when QbusClient disconnects */ private void eventDisconnect() { - CTDConnected = false; + ctdConnected = false; QbusBridgeHandler handler = bridgeCallBack; if (handler != null) { handler.bridgeOffline("No communication with Qbus client"); @@ -836,7 +849,7 @@ private void eventDisconnect() { * Put Bridge offline when there is no connection from the QbusClient */ private void noConnection() { - CTDConnected = false; + ctdConnected = false; QbusBridgeHandler handler = bridgeCallBack; if (handler != null) { handler.bridgeOffline("No communication with Qbus client"); @@ -847,7 +860,7 @@ private void noConnection() { * Set connection state true if there is a connection from QbusClient */ private void connection() { - CTDConnected = true; + ctdConnected = true; } /** diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java index 6b539aeb60408..a879ec7323856 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java @@ -29,7 +29,7 @@ public final class QbusDimmer { private final Logger logger = LoggerFactory.getLogger(QbusDimmer.class); - private @Nullable QbusCommunication QComm; + private @Nullable QbusCommunication qComm; private String id; @@ -67,14 +67,14 @@ public void setThingHandler(QbusDimmerHandler handler) { } /** - * This method sets a pointer to the QComm Dimmer of class {@link QbusCommuncation}. + * This method sets a pointer to the qComm Dimmer of class {@link QbusCommuncation}. * This is then used to be able to call back the sendCommand method in this class to send a command to the * Qbus IP-interface when.. * - * @param QComm + * @param qComm */ - public void setQComm(QbusCommunication QComm) { - this.QComm = QComm; + public void setQComm(QbusCommunication qComm) { + this.qComm = qComm; } /** @@ -107,13 +107,13 @@ void setState(int state) { * Sends Dimmer state to Qbus. */ public void execute(int percent, String sn) { - QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeDimmer").withId(this.id).withState(percent); - QbusCommunication comm = QComm; + QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeDimmer").withId(this.id).withState(percent); + QbusCommunication comm = qComm; if (comm != null) { try { - comm.sendMessage(QCmd); + comm.sendMessage(qCmd); } catch (InterruptedException e) { - logger.warn("Could not send command for dimmer {}", this.id); + logger.warn("Could not send command for dimmer {}, {}", this.id, e.getMessage()); } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java index 7ec1d2fdf304e..6c1f2365a8683 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java @@ -28,7 +28,7 @@ @NonNullByDefault abstract class QbusMessageBase { - private @Nullable String CTD; + private @Nullable String ctd; protected @Nullable String cmd; protected @Nullable String id; protected @Nullable Integer state; @@ -38,11 +38,11 @@ abstract class QbusMessageBase { @Nullable String getSn() { - return this.CTD; + return this.ctd; } - void setSn(String CTD) { - this.CTD = CTD; + void setSn(String cTD) { + this.ctd = cTD; } @Nullable diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java index 41c3c013b761f..746618be7c05f 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java @@ -47,14 +47,14 @@ class QbusMessageDeserializer implements JsonDeserializer { try { String cmd = null; - String CTD = null; + String ctd = null; if (jsonObject.has("cmd")) { cmd = jsonObject.get("cmd").getAsString(); } if (jsonObject.has("CTD")) { - CTD = jsonObject.get("CTD").getAsString(); + ctd = jsonObject.get("CTD").getAsString(); } JsonElement jsonOutputs = null; @@ -94,17 +94,17 @@ class QbusMessageDeserializer implements JsonDeserializer { } } - if (message != null && cmd != null && CTD != null) { + if (message != null && cmd != null && ctd != null) { message.setCmd(cmd); - message.setSn(CTD); + message.setSn(ctd); } else { - throw new JsonParseException("Unexpected Json type"); + throw new JsonParseException("Unexpected Json type "); } return message; } catch (IllegalStateException e) { - throw new JsonParseException("Unexpected Json type"); + throw new JsonParseException("Unexpected Json type {} ", e); } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java index b6190a23bb2e4..3e2646a2d0bb1 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java @@ -29,7 +29,7 @@ public final class QbusRol { private final Logger logger = LoggerFactory.getLogger(QbusRol.class); - private @Nullable QbusCommunication QComm; + private @Nullable QbusCommunication qComm; private String id; @@ -56,14 +56,14 @@ public void setThingHandler(QbusRolHandler qbusRolHandler) { } /** - * This method sets a pointer to the QComm Shutter/Slats of class {@link QbusCommuncation}. + * This method sets a pointer to the qComm Shutter/Slats of class {@link QbusCommuncation}. * This is then used to be able to call back the sendCommand method in this class to send a command to the * Qbus IP-interface when.. * - * @param QComm + * @param qComm */ - public void setQComm(QbusCommunication QComm) { - this.QComm = QComm; + public void setQComm(QbusCommunication qComm) { + this.qComm = qComm; } /** @@ -122,13 +122,13 @@ public void setSlats(Integer Slats) { * Sends shutter to Qbus. */ public void execute(int value, String sn) { - QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeStore").withId(this.id).withState(value); - QbusCommunication comm = QComm; + QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeStore").withId(this.id).withState(value); + QbusCommunication comm = qComm; if (comm != null) { try { - comm.sendMessage(QCmd); + comm.sendMessage(qCmd); } catch (InterruptedException e) { - logger.warn("Could not send command for store {}", this.id); + logger.warn("Could not send command for store {}, {}", this.id, e.getMessage()); } } } @@ -137,13 +137,13 @@ public void execute(int value, String sn) { * Sends slats to Qbus. */ public void executeSlats(int value, String sn) { - QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeSlats").withId(this.id).withSlatState(value); - QbusCommunication comm = QComm; + QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeSlats").withId(this.id).withSlatState(value); + QbusCommunication comm = qComm; if (comm != null) { try { - comm.sendMessage(QCmd); + comm.sendMessage(qCmd); } catch (InterruptedException e) { - logger.warn("Could not send command for slat {}", this.id); + logger.warn("Could not send command for slat {}, {}", this.id, e.getMessage()); } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java index 1e7f8d38f26d7..d693159c6aa59 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java @@ -29,7 +29,7 @@ public final class QbusScene { private final Logger logger = LoggerFactory.getLogger(QbusScene.class); - private @Nullable QbusCommunication QComm; + private @Nullable QbusCommunication qComm; public @Nullable QbusSceneHandler thingHandler; @@ -42,27 +42,27 @@ public final class QbusScene { } /** - * This method sets a pointer to the QComm Scene of class {@link QbusCommuncation}. + * This method sets a pointer to the qComm Scene of class {@link QbusCommuncation}. * This is then used to be able to call back the sendCommand method in this class to send a command to the * Qbus IP-interface when.. * - * @param QComm + * @param qComm */ - public void setQComm(QbusCommunication QComm) { - this.QComm = QComm; + public void setQComm(QbusCommunication qComm) { + this.qComm = qComm; } /** * Sends action to Qbus. */ public void execute(int value, String sn) { - QbusMessageCmd QCmd = new QbusMessageCmd(sn, "executeScene").withId(this.id).withState(value); - QbusCommunication comm = QComm; + QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeScene").withId(this.id).withState(value); + QbusCommunication comm = qComm; if (comm != null) { try { - comm.sendMessage(QCmd); + comm.sendMessage(qCmd); } catch (InterruptedException e) { - logger.warn("Could not send command for scene {}", this.id); + logger.warn("Could not send command for scene {}, {}", this.id, e.getMessage()); } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java index f874c2d3485bc..fa9b14654d727 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java @@ -151,7 +151,7 @@ public void executeMode(int mode, String sn) { try { comm.sendMessage(qCmd); } catch (InterruptedException e) { - logger.warn("Could not send command mode for thermostat {}", this.id); + logger.warn("Could not send command mode for thermostat {}, {}", this.id, e.getMessage()); } } } @@ -168,7 +168,7 @@ public void executeSetpoint(double d, String sn) { try { comm.sendMessage(qCmd); } catch (InterruptedException e) { - logger.warn("Could not send command setpoitn for thermostat {}", this.id); + logger.warn("Could not send command setpoitn for thermostat {}, {}", this.id, e.getMessage()); } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties index bca15973e1883..4368d800c6fd5 100644 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties @@ -1,6 +1,3 @@ -# FIXME: please substitute the xx_XX with a proper locale, ie. de_DE -# FIXME: please do not add the file to the repo if you add or change no content - # binding binding.qbus.name = Qbus Binding binding.qbus.description = Deze binding maakt via een server applicate verbinding met de Qbus controller. @@ -80,20 +77,9 @@ channel-type.qbus.mode.state.option.2 = Economisch channel-type.qbus.mode.state.option.3 = Comfort channel-type.qbus.mode.state.option.4 = Nacht - channel-type.qbus.rollershutter.label = Rolluik Bediening channel-type.qbus.rollershutter.description = Rolluik bediening van de uitgangen channel-type.qbus.slats.label = Lamellen Bediening channel-type.qbus.slats.description = Lamellen bediening van de uitgangen - - - -# thing type config description -thing-type.config.qbus.sample.config1.label = -thing-type.config.qbus.sample.config1.description = - -# channel types -channel-type.qbus.sample-channel.label = -channel-type.qbus.sample-channel.description = diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml index fd68182c27f35..46200599b8c94 100644 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml @@ -9,25 +9,25 @@ This bridge represents a Qbus client - - IP Address of Qbus Server, Usually 'localhost' + + IP address of Qbus server, usually 'localhost' localhost network-address - - Serial Number of the CTD Controller + + Serial number of the CTD controller - - Port To Communicate With Qbus Server, Default 8447 + + Port to communicate with Qbus server, default 8447 8447 true - - Refresh Interval for Connection With Qbus Server (min), default 10. If Set To 0 or Left Empty, No - Refresh Will Be Scheduled + + Refresh interval for connection with Qbus server (min), default 10. If set to 0 or left empty, no + refresh will be scheduled 10 true @@ -68,7 +68,6 @@ - @@ -91,7 +90,7 @@ - Qbus Dimmer Output + Qbus dimmer output @@ -101,22 +100,20 @@ Qbus Dimmer ID - - Step Value Used for Increase/Decrease of Dimmer Brightness, Default 10% + + Step value used for increase/decrease of dimmer brightness, default 10% 10 true - - - Qbus Shutter (ROL02P) Control + Qbus shutter (ROL02P) control @@ -133,7 +130,7 @@ - Qbus Shutter With Slats Control + Qbus shutter with slats control @@ -151,7 +148,7 @@ - Qbus Thermostat + Qbus thermostat @@ -168,35 +165,14 @@ Switch - Scene Control for Qbus + Scene control for Qbus Scene - - Number - - CO2 Value for Qbus - CO2 - - - - Switch - - Switch Control for Output in Qbus - Switch - - - - Dimmer - - Brightness Control for Dimmer Function in Qbus - DimmableLight - - Number:Temperature - Temperature Measured by Thermostat + Temperature measured by thermostat Temperature CurrentTemperature @@ -207,7 +183,7 @@ Number:Temperature - Setpoint Temperature of Thermostat + Setpoint temperature of thermostat Temperature TargetTemperature @@ -218,7 +194,7 @@ Number - Thermostat Mode + Thermostat mode Number @@ -231,17 +207,24 @@ + + Number + + CO2 value for Qbus + CO2 + + Rollershutter - Rollershutter Control for Qbus + Rollershutter control for Qbus Blinds Dimmer - Slatcontrol Function in Qbus + Slatcontrol for Qbus Blinds From f67dda391c2a8bdda78f96e956ffd920a925b4eb Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Wed, 24 Feb 2021 21:51:50 +0100 Subject: [PATCH 13/17] Updated requested changes Updated changes as requested by Fwolter on 24/02/2021 Signed-off-by: Koen Schockaert --- .../qbus/internal/QbusBridgeHandler.java | 81 +++++++--- .../handler/QbusBistabielHandler.java | 29 +++- .../qbus/internal/handler/QbusCO2Handler.java | 26 +++- .../internal/handler/QbusDimmerHandler.java | 25 +++- .../internal/handler/QbusGlobalHandler.java | 22 ++- .../qbus/internal/handler/QbusRolHandler.java | 35 +++-- .../internal/handler/QbusSceneHandler.java | 25 +++- .../handler/QbusThermostatHandler.java | 37 ++++- .../internal/handler/QbusThingsConfig.java | 13 +- .../internal/protocol/QbusCommunication.java | 138 ++++++++++-------- .../resources/OH-INF/thing/thing-types.xml | 4 +- 11 files changed, 310 insertions(+), 125 deletions(-) diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java index 823c74a7084fe..0168b2c8b45bf 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java @@ -40,14 +40,16 @@ @NonNullByDefault public class QbusBridgeHandler extends BaseBridgeHandler { - private final Logger logger = LoggerFactory.getLogger(QbusBridgeHandler.class); - private @Nullable QbusCommunication qbusComm; protected @Nullable QbusConfiguration config; private @Nullable ScheduledFuture refreshTimer; + private @Nullable ScheduledFuture pollingJob; + + private final Logger logger = LoggerFactory.getLogger(QbusBridgeHandler.class); + public QbusBridgeHandler(Bridge Bridge) { super(Bridge); } @@ -107,7 +109,7 @@ private void setBridgeCallBack() { private void createCommunicationObject(InetAddress addr, int port) { scheduler.submit(() -> { - setQbusCommunication(new QbusCommunication()); + setQbusCommunication(new QbusCommunication(thing)); QbusCommunication qbusCommunication = getQbusCommunication(); @@ -116,8 +118,7 @@ private void createCommunicationObject(InetAddress addr, int port) { try { qbusCommunication.startCommunication(); } catch (InterruptedException | IOException e) { - logger.warn("Error on restaring communication: {}", e.getMessage()); - bridgeOffline("Communication could not be established"); + bridgeOffline("Communication could not be established {}" + e.getMessage()); return; } @@ -176,7 +177,13 @@ private void setupRefreshTimer(int refreshInterval) { if (comm != null) { if (!comm.communicationActive()) { // Disconnected from Qbus Server, try to reconnect - comm.restartCommunication(); + try { + comm.restartCommunication(); + } catch (InterruptedException e) { + bridgeOffline("No connection with Qbus Server" + e.toString()); + } catch (IOException e) { + bridgeOffline("No connection with Qbus Server" + e.toString()); + } if (!comm.communicationActive()) { bridgeOffline("No connection with Qbus Server"); return; @@ -184,7 +191,13 @@ private void setupRefreshTimer(int refreshInterval) { } else { // Controller disconnected from Qbus client, try to reconnect controller if (!comm.clientConnected()) { - comm.restartCommunication(); + try { + comm.restartCommunication(); + } catch (InterruptedException e) { + bridgeOffline("No connection with Qbus Server" + e.toString()); + } catch (IOException e) { + bridgeOffline("No connection with Qbus Server" + e.toString()); + } if (!comm.clientConnected()) { bridgeOffline("No connection with Qbus Client"); return; @@ -210,8 +223,14 @@ public void dispose() { refreshTimer = null; QbusCommunication comm = getCommunication(); + if (comm != null) { - comm.stopCommunication(); + try { + comm.stopCommunication(); + } catch (IOException e) { + String message = e.toString(); + logger.error("Error on stopping communication.{} ", message); + } } comm = null; @@ -221,7 +240,15 @@ public void dispose() { * Sets the configuration parameters */ protected void readConfig() { - this.config = getConfig().as(QbusConfiguration.class); + final ScheduledFuture localPollingJob = this.pollingJob; + + if (localPollingJob != null) { + localPollingJob.cancel(true); + } + + if (localPollingJob == null || localPollingJob.isCancelled()) { + this.config = getConfig().as(QbusConfiguration.class); + } } /** @@ -239,8 +266,13 @@ protected void readConfig() { * @return the ip address */ public @Nullable String getAddress() { - if (this.config != null) { - return this.config.addr; + QbusConfiguration bridgeConfig = this.config; + if (bridgeConfig != null) { + if (bridgeConfig.addr != null) { + return bridgeConfig.addr; + } else { + return ""; + } } else { return null; } @@ -252,8 +284,13 @@ protected void readConfig() { * @return */ public @Nullable Integer getPort() { - if (this.config != null) { - return this.config.port; + QbusConfiguration bridgeConfig = this.config; + if (bridgeConfig != null) { + if (bridgeConfig.port != null) { + return bridgeConfig.port; + } else { + return 0; + } } else { return null; } @@ -265,8 +302,13 @@ protected void readConfig() { * @return the serial nr of the controller */ public @Nullable String getSn() { - if (this.config != null) { - return this.config.sn; + QbusConfiguration bridgeConfig = this.config; + if (bridgeConfig != null) { + if (bridgeConfig.sn != null) { + return bridgeConfig.sn; + } else { + return ""; + } } else { return null; } @@ -278,8 +320,13 @@ protected void readConfig() { * @return the refresh interval */ public @Nullable Integer getRefresh() { - if (this.config != null) { - return this.config.refresh; + QbusConfiguration bridgeConfig = this.config; + if (bridgeConfig != null) { + if (bridgeConfig.refresh != null) { + return bridgeConfig.refresh; + } else { + return 0; + } } else { return null; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java index 1ce96cf4ffaba..aa82f11fc3eea 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java @@ -16,6 +16,7 @@ import static org.openhab.core.types.RefreshType.REFRESH; import java.util.Map; +import java.util.concurrent.ScheduledFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -40,10 +41,12 @@ public class QbusBistabielHandler extends QbusGlobalHandler { protected @Nullable QbusThingsConfig config; - private int bistabielId; + private @Nullable Integer bistabielId; private @Nullable String sn; + private @Nullable ScheduledFuture pollingJob; + public QbusBistabielHandler(Thing thing) { super(thing); } @@ -162,10 +165,9 @@ public void handleCommand(ChannelUID channelUID, Command command) { */ private void handleSwitchCommand(QbusBistabiel qBistabiel, ChannelUID channelUID, Command command) { if (command instanceof OnOffType) { - OnOffType s = (OnOffType) command; String snr = getSN(); if (snr != null) { - if (s == OnOffType.OFF) { + if (command == OnOffType.OFF) { qBistabiel.execute(0, snr); } else { qBistabiel.execute(100, snr); @@ -192,7 +194,15 @@ public void handleStateUpdate(QbusBistabiel qBistabiel) { * Read the configuration */ protected synchronized void readConfig() { - this.config = getConfig().as(QbusThingsConfig.class); + final ScheduledFuture localPollingJob = this.pollingJob; + + if (localPollingJob != null) { + localPollingJob.cancel(true); + } + + if (localPollingJob == null || localPollingJob.isCancelled()) { + this.config = getConfig().as(QbusThingsConfig.class); + } } /** @@ -200,9 +210,14 @@ protected synchronized void readConfig() { * * @return */ - public int getId() { - if (this.config != null) { - return this.config.bistabielId; + public @Nullable Integer getId() { + QbusThingsConfig bistabielConfig = this.config; + if (bistabielConfig != null) { + if (bistabielConfig.bistabielId != null) { + return bistabielConfig.bistabielId; + } else { + return 0; + } } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java index f901fb284ae76..c38a22876752c 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java @@ -16,6 +16,7 @@ import static org.openhab.core.types.RefreshType.REFRESH; import java.util.Map; +import java.util.concurrent.ScheduledFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -40,10 +41,12 @@ public class QbusCO2Handler extends QbusGlobalHandler { protected @Nullable QbusThingsConfig config; - private int co2Id; + private @Nullable Integer co2Id; private @Nullable String sn; + private @Nullable ScheduledFuture pollingJob; + public QbusCO2Handler(Thing thing) { super(thing); } @@ -162,7 +165,15 @@ public void setSN() { * Read the configuration */ protected synchronized void setConfig() { - this.config = getConfig().as(QbusThingsConfig.class); + final ScheduledFuture localPollingJob = this.pollingJob; + + if (localPollingJob != null) { + localPollingJob.cancel(true); + } + + if (localPollingJob == null || localPollingJob.isCancelled()) { + this.config = getConfig().as(QbusThingsConfig.class); + } } /** @@ -170,9 +181,14 @@ protected synchronized void setConfig() { * * @return co2Id */ - public int getId() { - if (this.config != null) { - return this.config.co2Id; + public @Nullable Integer getId() { + QbusThingsConfig co2Config = this.config; + if (co2Config != null) { + if (co2Config.co2Id != null) { + return co2Config.co2Id; + } else { + return 0; + } } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java index eb26fe7487db1..d3df1b362a23a 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java @@ -16,6 +16,7 @@ import static org.openhab.core.types.RefreshType.REFRESH; import java.util.Map; +import java.util.concurrent.ScheduledFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -42,10 +43,12 @@ public class QbusDimmerHandler extends QbusGlobalHandler { protected @Nullable QbusThingsConfig config; - private int dimmerId; + private @Nullable Integer dimmerId; private @Nullable String sn; + private @Nullable ScheduledFuture pollingJob; + public QbusDimmerHandler(Thing thing) { super(thing); } @@ -274,6 +277,15 @@ public void handleStateUpdate(QbusDimmer qDimmer) { * Read the configuration */ protected synchronized void setConfig() { + final ScheduledFuture localPollingJob = this.pollingJob; + + if (localPollingJob != null) { + localPollingJob.cancel(true); + } + + if (localPollingJob == null || localPollingJob.isCancelled()) { + this.config = getConfig().as(QbusThingsConfig.class); + } this.config = getConfig().as(QbusThingsConfig.class); } @@ -282,9 +294,14 @@ protected synchronized void setConfig() { * * @return dimmerId */ - public int getId() { - if (this.config != null) { - return this.config.dimmerId; + public @Nullable Integer getId() { + QbusThingsConfig dimmerConfig = this.config; + if (dimmerConfig != null) { + if (dimmerConfig.dimmerId != null) { + return dimmerConfig.dimmerId; + } else { + return 0; + } } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java index 072c1aecfc32f..05d67b5f12f69 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java @@ -12,6 +12,8 @@ */ package org.openhab.binding.qbus.internal.handler; +import java.io.IOException; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.QbusBridgeHandler; @@ -42,8 +44,12 @@ public QbusGlobalHandler(Thing thing) { * @param globalId * @return */ - public @Nullable QbusCommunication getCommunication(String type, int globalId) { - QbusBridgeHandler qBridgeHandler = getBridgeHandler(type, globalId); + public @Nullable QbusCommunication getCommunication(String type, @Nullable Integer globalId) { + QbusBridgeHandler qBridgeHandler = null; + if (globalId != null) { + qBridgeHandler = getBridgeHandler(type, globalId); + } + if (qBridgeHandler == null) { updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED, "No bridge handler initialized for " + type + " with id " + globalId + "."); @@ -60,7 +66,7 @@ public QbusGlobalHandler(Thing thing) { * @param globalId * @return */ - public @Nullable QbusBridgeHandler getBridgeHandler(String type, int globalId) { + public @Nullable QbusBridgeHandler getBridgeHandler(String type, @Nullable Integer globalId) { Bridge qBridge = getBridge(); if (qBridge == null) { updateStatus(ThingStatus.UNKNOWN, ThingStatusDetail.BRIDGE_UNINITIALIZED, @@ -77,8 +83,14 @@ public QbusGlobalHandler(Thing thing) { * @param type * @param globalId */ - public void restartCommunication(QbusCommunication qComm, String type, int globalId) { - qComm.restartCommunication(); + public void restartCommunication(QbusCommunication qComm, String type, @Nullable Integer globalId) { + try { + qComm.restartCommunication(); + } catch (InterruptedException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.toString()); + } catch (IOException e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.toString()); + } if (!qComm.communicationActive()) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Communication socket error"); diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java index 391487e335a74..18fe817f2f8d0 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java @@ -17,6 +17,7 @@ import static org.openhab.core.types.RefreshType.REFRESH; import java.util.Map; +import java.util.concurrent.ScheduledFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -44,10 +45,12 @@ public class QbusRolHandler extends QbusGlobalHandler { protected @Nullable QbusThingsConfig config; - private int rolId; + private @Nullable Integer rolId; private @Nullable String sn; + private @Nullable ScheduledFuture pollingJob; + public QbusRolHandler(Thing thing) { super(thing); } @@ -242,9 +245,8 @@ private void handleScreenposCommand(QbusRol qRol, Command command) { */ private void handleSlatsposCommand(QbusRol qRol, Command command) { String snr = getSN(); - if (command instanceof org.openhab.core.library.types.UpDownType) { - org.openhab.core.library.types.UpDownType s = (org.openhab.core.library.types.UpDownType) command; - if (s == org.openhab.core.library.types.UpDownType.DOWN) { + if (command instanceof UpDownType) { + if (command == DOWN) { if (snr != null) { qRol.executeSlats(0, snr); } else { @@ -258,13 +260,12 @@ private void handleSlatsposCommand(QbusRol qRol, Command command) { } } } else if (command instanceof IncreaseDecreaseType) { - IncreaseDecreaseType s = (IncreaseDecreaseType) command; int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); Integer currentValue = qRol.getState(); int newValue; int sendValue; if (currentValue != null) { - if (s == IncreaseDecreaseType.INCREASE) { + if (command == IncreaseDecreaseType.INCREASE) { newValue = currentValue + stepValue; // round down to step multiple newValue = newValue - newValue % stepValue; @@ -289,7 +290,7 @@ private void handleSlatsposCommand(QbusRol qRol, Command command) { } else if (command instanceof PercentType) { PercentType p = (PercentType) command; int pp = p.intValue(); - if (p == PercentType.ZERO) { + if (command == PercentType.ZERO) { if (snr != null) { qRol.executeSlats(0, snr); } else { @@ -326,6 +327,15 @@ public void handleStateUpdate(QbusRol qRol) { * Read the configuration */ protected synchronized void setConfig() { + final ScheduledFuture localPollingJob = this.pollingJob; + + if (localPollingJob != null) { + localPollingJob.cancel(true); + } + + if (localPollingJob == null || localPollingJob.isCancelled()) { + this.config = getConfig().as(QbusThingsConfig.class); + } this.config = getConfig().as(QbusThingsConfig.class); } @@ -334,9 +344,14 @@ protected synchronized void setConfig() { * * @return rolId */ - public int getId() { - if (this.config != null) { - return this.config.rolId; + public @Nullable Integer getId() { + QbusThingsConfig rolConfig = this.config; + if (rolConfig != null) { + if (rolConfig.rolId != null) { + return rolConfig.rolId; + } else { + return 0; + } } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java index d8962dffd4a8b..0b0a3ff15548a 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java @@ -15,6 +15,7 @@ import static org.openhab.binding.qbus.internal.QbusBindingConstants.CHANNEL_SCENE; import java.util.Map; +import java.util.concurrent.ScheduledFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -40,10 +41,12 @@ public class QbusSceneHandler extends QbusGlobalHandler { protected @Nullable QbusThingsConfig config; - private int sceneId; + private @Nullable Integer sceneId; private @Nullable String sn; + private @Nullable ScheduledFuture pollingJob; + public QbusSceneHandler(Thing thing) { super(thing); } @@ -194,6 +197,15 @@ void handleSwitchCommand(QbusScene qScene, ChannelUID channelUID, Command comman * Read the configuration */ protected synchronized void setConfig() { + final ScheduledFuture localPollingJob = this.pollingJob; + + if (localPollingJob != null) { + localPollingJob.cancel(true); + } + + if (localPollingJob == null || localPollingJob.isCancelled()) { + this.config = getConfig().as(QbusThingsConfig.class); + } this.config = getConfig().as(QbusThingsConfig.class); } @@ -202,9 +214,14 @@ protected synchronized void setConfig() { * * @return sceneId */ - public int getId() { - if (this.config != null) { - return this.config.sceneId; + public @Nullable Integer getId() { + QbusThingsConfig sceneConfig = this.config; + if (sceneConfig != null) { + if (sceneConfig.sceneId != null) { + return sceneConfig.sceneId; + } else { + return 0; + } } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java index 052ef994c91e4..414312f349820 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java @@ -17,6 +17,7 @@ import static org.openhab.core.types.RefreshType.REFRESH; import java.util.Map; +import java.util.concurrent.ScheduledFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -43,10 +44,12 @@ public class QbusThermostatHandler extends QbusGlobalHandler { protected @Nullable QbusThingsConfig config; - private int thermostatId; + private @Nullable Integer thermostatId; private @Nullable String sn; + private @Nullable ScheduledFuture pollingJob; + public QbusThermostatHandler(Thing thing) { super(thing); } @@ -196,9 +199,14 @@ private void handleSetpointCommand(QbusThermostat qThermostat, Command command) if (command instanceof QuantityType) { QuantityType s = (QuantityType) command; double sp = s.doubleValue(); - s.toUnit(CELSIUS); + QuantityType spCelcius = s.toUnit(CELSIUS); + if (snr != null) { - qThermostat.executeSetpoint(sp, snr); + if (spCelcius != null) { + qThermostat.executeSetpoint(sp, snr); + } else { + thingOffline("Could not set setpoint for thermostat (convertion failed) " + thermostatId); + } } else { thingOffline("No serial number configured for " + thermostatId); } @@ -226,17 +234,30 @@ public void handleStateUpdate(QbusThermostat qThermostat) { * Read the configuration */ protected synchronized void setConfig() { - this.config = getConfig().as(QbusThingsConfig.class); + final ScheduledFuture localPollingJob = this.pollingJob; + + if (localPollingJob != null) { + localPollingJob.cancel(true); + } + + if (localPollingJob == null || localPollingJob.isCancelled()) { + this.config = getConfig().as(QbusThingsConfig.class); + } } /** * Returns the Id from the configuration * - * @return dimmerId + * @return thermostatId */ - public int getId() { - if (this.config != null) { - return this.config.thermostatId; + public @Nullable Integer getId() { + QbusThingsConfig thConfig = this.config; + if (thConfig != null) { + if (thConfig.thermostatId != null) { + return thConfig.thermostatId; + } else { + return 0; + } } else { return 0; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java index 49cda1d052c47..56de0cb64d8f4 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThingsConfig.java @@ -13,6 +13,7 @@ package org.openhab.binding.qbus.internal.handler; import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; /** * The {@link QbusThingsConfig} is responible for handling configurations for all things @@ -22,10 +23,10 @@ @NonNullByDefault public class QbusThingsConfig { - public int bistabielId; - public int dimmerId; - public int co2Id; - public int rolId; - public int sceneId; - public int thermostatId; + public @Nullable Integer bistabielId; + public @Nullable Integer dimmerId; + public @Nullable Integer co2Id; + public @Nullable Integer rolId; + public @Nullable Integer sceneId; + public @Nullable Integer thermostatId; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java index f07540a523012..f625ce8e4870a 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java @@ -29,6 +29,10 @@ import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.QbusBridgeHandler; import org.openhab.core.common.NamedThreadFactory; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.types.Command; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -52,7 +56,14 @@ */ @NonNullByDefault -public final class QbusCommunication { +public final class QbusCommunication extends BaseThingHandler { + + public QbusCommunication(Thing thing) { + super(thing); + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(QbusMessageBase.class, new QbusMessageDeserializer()); + gsonIn = gsonBuilder.create(); + } private final Logger logger = LoggerFactory.getLogger(QbusCommunication.class); @@ -77,32 +88,10 @@ public final class QbusCommunication { private final Map co2 = new HashMap<>(); private final ExecutorService threadExecutor = Executors - .newSingleThreadExecutor(new NamedThreadFactory("org.openhab.binding.qbus", true)); + .newSingleThreadExecutor(new NamedThreadFactory(getThing().getUID().getAsString(), true)); private @Nullable QbusBridgeHandler bridgeCallBack; - /** - * Constructor for Qbus communication object, manages communication with - * Qbus Server. - * - */ - public QbusCommunication() { - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.registerTypeAdapter(QbusMessageBase.class, new QbusMessageDeserializer()); - gsonIn = gsonBuilder.create(); - } - - /** - * Start communication with Qbus Server, run through initialization and start thread listening - * to all messages coming from Qbus. - * - * @param addr : IP-address of Qbus Server - * @param port : Communication port of Qbus server - * @throws InterruptedException - * @throws IOException - * - */ - public synchronized void startCommunication() throws IOException, InterruptedException { QbusBridgeHandler handler = bridgeCallBack; ctdConnected = false; @@ -157,7 +146,7 @@ public synchronized void startCommunication() throws IOException, InterruptedExc * @throws IOException * */ - public synchronized void stopCommunication() { + public synchronized void stopCommunication() throws IOException { listenerStopped = true; Socket socket = qSocket; @@ -171,17 +160,21 @@ public synchronized void stopCommunication() { } qSocket = null; - if (qOut != null) { - qOut.close(); + + PrintWriter qbusOut = this.qOut; + if (qbusOut != null) { + qbusOut.close(); } + BufferedReader qbusIn = this.qIn; try { - if (qIn != null) { - qIn.close(); + if (qbusIn != null) { + qbusIn.close(); } } catch (IOException e) { logger.debug("Error on closing reader {} ", e.getMessage()); } + ctdConnected = false; logger.debug("Communication stopped from thread {}", Thread.currentThread().getId()); } @@ -190,8 +183,10 @@ public synchronized void stopCommunication() { * Close and restart communication with Qbus Server. * * @throws InterruptedException + * @throws IOException */ - public synchronized void restartCommunication() { + + public synchronized void restartCommunication() throws InterruptedException, IOException { stopCommunication(); logger.debug("Qbus: restart communication from thread {}", Thread.currentThread().getId()); @@ -199,6 +194,8 @@ public synchronized void restartCommunication() { try { startCommunication(); } catch (InterruptedException | IOException e) { + String message = e.toString(); + logger.error("Could not start the communication with the server. {}", message); } } @@ -235,22 +232,21 @@ public boolean clientConnected() { private void qEvents() throws IOException { String qMessage; - logger.info("Listening for events on thread {}", Thread.currentThread().getId()); listenerStopped = false; qEventsRunning = true; - BufferedReader reader = qIn; + BufferedReader reader = this.qIn; if (reader == null) { - throw new IOException("Bufferreader for incomming messages not initialized."); + throw new IOException("Bufferreader for incoming messages not initialized."); } + while (!Thread.currentThread().isInterrupted() && ((qMessage = reader.readLine()) != null)) { readMessage(qMessage); } if (!listenerStopped) { qEventsRunning = false; - logger.debug("Qbus: IO error in listener on thread {}", Thread.currentThread().getId()); QbusBridgeHandler handler = bridgeCallBack; @@ -273,20 +269,27 @@ private void qEvents() throws IOException { synchronized void sendMessage(Object qMessage) throws InterruptedException { PrintWriter writer = qOut; String json = gsonOut.toJson(qMessage); + QbusBridgeHandler handler = bridgeCallBack; if (writer != null) { writer.println(json); // Delay after sending data to improve scene execution - try { - TimeUnit.MILLISECONDS.sleep(250); - } catch (InterruptedException e) { - throw new InterruptedException(e.toString()); - } + TimeUnit.MILLISECONDS.sleep(250); } if ((writer == null) || (writer.checkError())) { logger.warn("Error sending message, trying to restart communication"); - restartCommunication(); + try { + restartCommunication(); + } catch (InterruptedException e) { + if (handler != null) { + handler.bridgeOffline("No communication with Qbus server. " + e.toString()); + } + } catch (IOException e) { + if (handler != null) { + handler.bridgeOffline("No communication with Qbus server. " + e.toString()); + } + } // retry sending after restart writer = qOut; if (writer != null) { @@ -339,8 +342,10 @@ private void readMessage(String qMessage) { cmd = qMessageGson.getCmd(); } - if (bridgeCallBack != null) { - sn = bridgeCallBack.getSn(); + QbusBridgeHandler handler = bridgeCallBack; + + if (handler != null) { + sn = handler.getSn(); } if (sn != null && ctd != null) { @@ -404,34 +409,49 @@ else if ("disconnect".equals(cmd)) { * Get request for Shutters * Get request for Thermostats * Get request for CO2 - * - * - * @throws IOException - * @throws InterruptedException */ + @Override + public void initialize() { + QbusBridgeHandler handler = bridgeCallBack; - private void initialize() throws IOException, InterruptedException { if (bridgeCallBack != null) { if (ctdConnected) { - sendAndReadMessage("getBistabiel"); - sendAndReadMessage("getScene"); - sendAndReadMessage("getDimmer"); - sendAndReadMessage("getRol02p"); - sendAndReadMessage("getRol02pSlat"); - sendAndReadMessage("getThermostat"); - sendAndReadMessage("getCo2"); + try { + sendAndReadMessage("getBistabiel"); + sendAndReadMessage("getScene"); + sendAndReadMessage("getDimmer"); + sendAndReadMessage("getRol02p"); + sendAndReadMessage("getRol02pSlat"); + sendAndReadMessage("getThermostat"); + sendAndReadMessage("getCo2"); + } catch (IOException e) { + String message = e.toString(); + if (handler != null) { + handler.bridgeOffline("Could not request outputs from client. {}" + message); + } else { + logger.error("Could not request outputs from client. {}", message); + } + } catch (InterruptedException e) { + String message = e.toString(); + if (handler != null) { + handler.bridgeOffline("Could not request outputs from client. {}" + message); + } else { + logger.error("Could not request outputs from client. {}", message); + } + } + } else { ctdConnected = false; - QbusBridgeHandler handler = bridgeCallBack; - if (handler != null) { handler.bridgeOffline("No communication with Qbus client"); } return; } - } else { + } else + + { logger.trace("Initialization error"); } } @@ -922,4 +942,8 @@ private void connection() { public void setBridgeCallBack(QbusBridgeHandler bridgeCallBack) { this.bridgeCallBack = bridgeCallBack; } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } } diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml index f604a3441d1bb..16c93351e4548 100644 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml @@ -9,8 +9,8 @@ This bridge represents a Qbus client - - IP address of Qbus server, usually 'localhost' + + IP address or hostname of Qbus server, usually 'localhost' localhost network-address From 34536bd3cbeeeb8e309e5e68169682f2aea3765a Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Thu, 25 Feb 2021 18:45:48 +0100 Subject: [PATCH 14/17] Updated requested changes Updated changes reqeusted by fwolter on 25/02/2021 Signed-off-by: Koen Schockaert --- .../binding/qbus/internal/QbusBridgeHandler.java | 12 ++++++------ .../qbus/internal/handler/QbusDimmerHandler.java | 5 ++--- .../qbus/internal/handler/QbusGlobalHandler.java | 10 ++++++++-- .../qbus/internal/handler/QbusRolHandler.java | 5 ++--- .../qbus/internal/handler/QbusSceneHandler.java | 3 +-- .../internal/handler/QbusThermostatHandler.java | 6 +++++- .../qbus/internal/protocol/QbusCommunication.java | 14 ++++---------- 7 files changed, 28 insertions(+), 27 deletions(-) diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java index 0168b2c8b45bf..badc5e9087d0c 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java @@ -118,7 +118,7 @@ private void createCommunicationObject(InetAddress addr, int port) { try { qbusCommunication.startCommunication(); } catch (InterruptedException | IOException e) { - bridgeOffline("Communication could not be established {}" + e.getMessage()); + bridgeOffline("Communication could not be established " + e.getMessage()); return; } @@ -180,9 +180,9 @@ private void setupRefreshTimer(int refreshInterval) { try { comm.restartCommunication(); } catch (InterruptedException e) { - bridgeOffline("No connection with Qbus Server" + e.toString()); + bridgeOffline("No connection with Qbus Server " + e.toString()); } catch (IOException e) { - bridgeOffline("No connection with Qbus Server" + e.toString()); + bridgeOffline("No connection with Qbus Server " + e.toString()); } if (!comm.communicationActive()) { bridgeOffline("No connection with Qbus Server"); @@ -194,9 +194,9 @@ private void setupRefreshTimer(int refreshInterval) { try { comm.restartCommunication(); } catch (InterruptedException e) { - bridgeOffline("No connection with Qbus Server" + e.toString()); + bridgeOffline("No connection with Qbus Server " + e.toString()); } catch (IOException e) { - bridgeOffline("No connection with Qbus Server" + e.toString()); + bridgeOffline("No connection with Qbus Server " + e.toString()); } if (!comm.clientConnected()) { bridgeOffline("No connection with Qbus Client"); @@ -229,7 +229,7 @@ public void dispose() { comm.stopCommunication(); } catch (IOException e) { String message = e.toString(); - logger.error("Error on stopping communication.{} ", message); + logger.debug("Error on stopping communication.{} ", message); } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java index d3df1b362a23a..e5031105bd435 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java @@ -244,8 +244,7 @@ private void handleBrightnessCommand(QbusDimmer qDimmer, Command command) { } } } else if (command instanceof PercentType) { - PercentType p = (PercentType) command; - int pp = p.intValue(); + int percentToInt = ((PercentType) command).intValue(); if (command == PercentType.ZERO) { if (snr != null) { qDimmer.execute(0, snr); @@ -254,7 +253,7 @@ private void handleBrightnessCommand(QbusDimmer qDimmer, Command command) { } } else { if (snr != null) { - qDimmer.execute(pp, snr); + qDimmer.execute(percentToInt, snr); } else { thingOffline("No serial number configured for " + dimmerId); } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java index 05d67b5f12f69..7a9ee95bcf180 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java @@ -23,6 +23,8 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseThingHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link QbusGlobalHandler} is used in other handlers, to share the functions. @@ -33,6 +35,8 @@ @NonNullByDefault public abstract class QbusGlobalHandler extends BaseThingHandler { + private final Logger logger = LoggerFactory.getLogger(QbusGlobalHandler.class); + public QbusGlobalHandler(Thing thing) { super(thing); } @@ -87,9 +91,11 @@ public void restartCommunication(QbusCommunication qComm, String type, @Nullable try { qComm.restartCommunication(); } catch (InterruptedException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.toString()); + String message = e.toString(); + logger.debug("Error on stopping communication.{} ", message); } catch (IOException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.toString()); + String message = e.toString(); + logger.debug("Error on stopping communication.{} ", message); } if (!qComm.communicationActive()) { diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java index 18fe817f2f8d0..19eeebe9e3a37 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java @@ -288,8 +288,7 @@ private void handleSlatsposCommand(QbusRol qRol, Command command) { } } } else if (command instanceof PercentType) { - PercentType p = (PercentType) command; - int pp = p.intValue(); + int percentToInt = ((PercentType) command).intValue(); if (command == PercentType.ZERO) { if (snr != null) { qRol.executeSlats(0, snr); @@ -298,7 +297,7 @@ private void handleSlatsposCommand(QbusRol qRol, Command command) { } } else { if (snr != null) { - qRol.executeSlats(pp, snr); + qRol.executeSlats(percentToInt, snr); } else { thingOffline("No serial number configured for " + rolId); } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java index 0b0a3ff15548a..88924a12b90cd 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java @@ -176,8 +176,7 @@ public void thingOffline(String message) { void handleSwitchCommand(QbusScene qScene, ChannelUID channelUID, Command command) { String snr = getSN(); if (command instanceof OnOffType) { - OnOffType s = (OnOffType) command; - if (s == OnOffType.OFF) { + if (command == OnOffType.OFF) { if (snr != null) { qScene.execute(0, snr); } else { diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java index 414312f349820..0d90eb9b34a12 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java @@ -31,6 +31,8 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link QbusThermostatHandler} is responsible for handling commands, which are @@ -42,6 +44,8 @@ @NonNullByDefault public class QbusThermostatHandler extends QbusGlobalHandler { + private final Logger logger = LoggerFactory.getLogger(QbusThermostatHandler.class); + protected @Nullable QbusThingsConfig config; private @Nullable Integer thermostatId; @@ -205,7 +209,7 @@ private void handleSetpointCommand(QbusThermostat qThermostat, Command command) if (spCelcius != null) { qThermostat.executeSetpoint(sp, snr); } else { - thingOffline("Could not set setpoint for thermostat (convertion failed) " + thermostatId); + logger.debug("Could not set setpoint for thermostat (conversion failed) {}", thermostatId); } } else { thingOffline("No serial number configured for " + thermostatId); diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java index f625ce8e4870a..7e63b8ff6d8bf 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java @@ -195,7 +195,7 @@ public synchronized void restartCommunication() throws InterruptedException, IOE startCommunication(); } catch (InterruptedException | IOException e) { String message = e.toString(); - logger.error("Could not start the communication with the server. {}", message); + logger.debug("Could not start the communication with the server. {}", message); } } @@ -281,10 +281,6 @@ synchronized void sendMessage(Object qMessage) throws InterruptedException { logger.warn("Error sending message, trying to restart communication"); try { restartCommunication(); - } catch (InterruptedException e) { - if (handler != null) { - handler.bridgeOffline("No communication with Qbus server. " + e.toString()); - } } catch (IOException e) { if (handler != null) { handler.bridgeOffline("No communication with Qbus server. " + e.toString()); @@ -429,14 +425,14 @@ public void initialize() { if (handler != null) { handler.bridgeOffline("Could not request outputs from client. {}" + message); } else { - logger.error("Could not request outputs from client. {}", message); + logger.debug("Could not request outputs from client. {}", message); } } catch (InterruptedException e) { String message = e.toString(); if (handler != null) { handler.bridgeOffline("Could not request outputs from client. {}" + message); } else { - logger.error("Could not request outputs from client. {}", message); + logger.debug("Could not request outputs from client. {}", message); } } @@ -449,9 +445,7 @@ public void initialize() { return; } - } else - - { + } else { logger.trace("Initialization error"); } } From 0c551c6032d6cdfe42d7ae84e2289b196b157159 Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Tue, 23 Mar 2021 10:48:49 +0100 Subject: [PATCH 15/17] Updated requested changes Updated changes as requested by fwolter on 2/3/2021 Signed-off-by: Koen Schockaert --- .../qbus/internal/QbusBindingConstants.java | 1 + .../qbus/internal/QbusBridgeHandler.java | 250 ++--- .../qbus/internal/QbusConfiguration.java | 2 +- .../handler/QbusBistabielHandler.java | 201 ++-- .../qbus/internal/handler/QbusCO2Handler.java | 129 +-- .../internal/handler/QbusDimmerHandler.java | 296 +++--- .../internal/handler/QbusGlobalHandler.java | 27 +- .../qbus/internal/handler/QbusRolHandler.java | 335 +++---- .../internal/handler/QbusSceneHandler.java | 203 ++-- .../handler/QbusThermostatHandler.java | 258 ++--- .../qbus/internal/protocol/QbusBistabiel.java | 55 +- .../qbus/internal/protocol/QbusCO2.java | 30 +- .../internal/protocol/QbusCommunication.java | 933 ++++++++---------- .../qbus/internal/protocol/QbusDimmer.java | 66 +- .../internal/protocol/QbusMessageBase.java | 30 +- .../internal/protocol/QbusMessageCmd.java | 2 +- .../protocol/QbusMessageDeserializer.java | 115 ++- .../qbus/internal/protocol/QbusRol.java | 92 +- .../qbus/internal/protocol/QbusScene.java | 64 +- .../internal/protocol/QbusThermostat.java | 121 +-- .../resources/OH-INF/i18n/qbus_nl.properties | 4 +- .../resources/OH-INF/thing/thing-types.xml | 9 +- 22 files changed, 1566 insertions(+), 1657 deletions(-) diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java index cf2f9d983f27e..57b0cf5575a78 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBindingConstants.java @@ -36,6 +36,7 @@ public class QbusBindingConstants { public static final String CONFIG_HOST_NAME = "addr"; public static final String CONFIG_PORT = "port"; public static final String CONFIG_SN = "sn"; + public static final String CONFIG_SERVERCHECK = "serverCheck"; // generic thing types public static final ThingTypeUID THING_TYPE_CO2 = new ThingTypeUID(BINDING_ID, "co2"); diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java index badc5e9087d0c..30d12112aec82 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java @@ -26,6 +26,7 @@ import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.ThingStatusInfo; import org.openhab.core.thing.binding.BaseBridgeHandler; import org.openhab.core.types.Command; import org.slf4j.Logger; @@ -42,12 +43,10 @@ public class QbusBridgeHandler extends BaseBridgeHandler { private @Nullable QbusCommunication qbusComm; - protected @Nullable QbusConfiguration config; + protected @Nullable QbusConfiguration bridgeConfig = new QbusConfiguration(); private @Nullable ScheduledFuture refreshTimer; - private @Nullable ScheduledFuture pollingJob; - private final Logger logger = LoggerFactory.getLogger(QbusBridgeHandler.class); public QbusBridgeHandler(Bridge Bridge) { @@ -63,7 +62,7 @@ public void initialize() { InetAddress addr; Integer port = getPort(); - Integer refresh = getRefresh(); + Integer serverCheck = getServerCheck(); if (port == null) { updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, @@ -85,8 +84,8 @@ public void initialize() { "Incorrect ip address set for Qbus Server"); } - if (refresh != null) { - this.setupRefreshTimer(refresh); + if (serverCheck != null) { + this.setupRefreshTimer(serverCheck); } } @@ -108,42 +107,69 @@ private void setBridgeCallBack() { */ private void createCommunicationObject(InetAddress addr, int port) { scheduler.submit(() -> { - setQbusCommunication(new QbusCommunication(thing)); QbusCommunication qbusCommunication = getQbusCommunication(); setBridgeCallBack(); - if (qbusCommunication != null) { - try { - qbusCommunication.startCommunication(); - } catch (InterruptedException | IOException e) { - bridgeOffline("Communication could not be established " + e.getMessage()); - return; - } - if (!qbusCommunication.communicationActive()) { - bridgeOffline("No communication with Qbus Server"); - return; - } + Integer serverCheck = getServerCheck(); + String sn = getSn(); + if (serverCheck != null) { + if (sn != null) { + if (qbusCommunication != null) { + try { + qbusCommunication.startCommunication(); + } catch (InterruptedException e) { + String msg = e.getMessage(); + bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Communication wit Qbus server could not be established, will try to reconnect every " + + serverCheck + " minutes. InterruptedException: " + msg); + return; + } catch (IOException e) { + String msg = e.getMessage(); + bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Communication wit Qbus server could not be established, will try to reconnect every " + + serverCheck + " minutes. IOException: " + msg); + return; + } - if (!qbusCommunication.clientConnected()) { - bridgeOffline("No communication with Qbus Client"); - return; - } - } + if (!qbusCommunication.communicationActive()) { + bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "No communication with Qbus Server, will try to reconnect every " + serverCheck + + " minutes"); + return; + } - updateStatus(ThingStatus.ONLINE); + if (!qbusCommunication.clientConnected()) { + bridgePending("Waiting for Qbus client to come online"); + return; + } + } + } + } }); } /** - * Take bridge offline when error in communication with Qbus server. This method can also be - * called directly from {@link QbusCommunication} object. + * Updates offline status off the Bridge when an error occurs. + * + * @param status + * @param detail + * @param message + */ + public void bridgeOffline(ThingStatusDetail detail, String message) { + updateStatus(ThingStatus.OFFLINE, detail, message); + } + + /** + * Updates pending status off the Bridge (usualay when Qbus client id not connected) + * + * @param message */ - public void bridgeOffline(String message) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); + public void bridgePending(String message) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_CONFIGURATION_PENDING, message); } /** @@ -154,13 +180,13 @@ public void bridgeOnline() { } /** - * Schedule future communication refresh. + * Initializes a timer that check the communication with Qbus server/client and tries to re-establish communication. * - * @param interval_config Time before refresh in minutes. + * @param refreshInterval Time before refresh in minutes. */ - private void setupRefreshTimer(int refreshInterval) { ScheduledFuture timer = refreshTimer; + if (timer != null) { timer.cancel(true); refreshTimer = null; @@ -170,43 +196,30 @@ private void setupRefreshTimer(int refreshInterval) { return; } - // This timer will check connection with server and client periodically refreshTimer = scheduler.scheduleWithFixedDelay(() -> { QbusCommunication comm = getCommunication(); + Integer serverCheck = getServerCheck(); if (comm != null) { - if (!comm.communicationActive()) { - // Disconnected from Qbus Server, try to reconnect - try { - comm.restartCommunication(); - } catch (InterruptedException e) { - bridgeOffline("No connection with Qbus Server " + e.toString()); - } catch (IOException e) { - bridgeOffline("No connection with Qbus Server " + e.toString()); - } + if (serverCheck != null) { if (!comm.communicationActive()) { - bridgeOffline("No connection with Qbus Server"); - return; - } - } else { - // Controller disconnected from Qbus client, try to reconnect controller - if (!comm.clientConnected()) { + // Disconnected from Qbus Server, restart communication try { - comm.restartCommunication(); + comm.startCommunication(); } catch (InterruptedException e) { - bridgeOffline("No connection with Qbus Server " + e.toString()); + String msg = e.getMessage(); + bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Communication wit Qbus server could not be established, will try to reconnect every " + + serverCheck + " minutes. InterruptedException: " + msg); } catch (IOException e) { - bridgeOffline("No connection with Qbus Server " + e.toString()); - } - if (!comm.clientConnected()) { - bridgeOffline("No connection with Qbus Client"); - return; + String msg = e.getMessage(); + bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Communication wit Qbus server could not be established, will try to reconnect every " + + serverCheck + " minutes. IOException: " + msg); } } } } - updateStatus(ThingStatus.ONLINE); - }, refreshInterval, refreshInterval, TimeUnit.MINUTES); } @@ -237,18 +250,59 @@ public void dispose() { } /** - * Sets the configuration parameters + * Reconnect to Qbus server if controller is offline */ - protected void readConfig() { - final ScheduledFuture localPollingJob = this.pollingJob; + public void ctdOffline() { + bridgePending("Waiting for CTD connection"); + } - if (localPollingJob != null) { - localPollingJob.cancel(true); + /** + * Get BridgeCommunication + * + * @return BridgeCommunication + */ + public @Nullable QbusCommunication getQbusCommunication() { + if (this.qbusComm != null) { + return this.qbusComm; + } else { + return null; } + } - if (localPollingJob == null || localPollingJob.isCancelled()) { - this.config = getConfig().as(QbusConfiguration.class); - } + /** + * Sets BridgeCommunication + * + * @param BridgeCommunication + */ + void setQbusCommunication(QbusCommunication comm) { + this.qbusComm = comm; + } + + /** + * Gets the status off the Bridge + * + * @return + */ + public ThingStatus getStatus() { + return thing.getStatus(); + } + + /** + * Gets the status off the Bridge + * + * @return + */ + public ThingStatusDetail getStatusDetails() { + ThingStatusInfo status = thing.getStatusInfo(); + ThingStatusDetail detail = status.getStatusDetail(); + return detail; + } + + /** + * Sets the configuration parameters + */ + protected void readConfig() { + bridgeConfig = getConfig().as(QbusConfiguration.class); } /** @@ -266,13 +320,10 @@ protected void readConfig() { * @return the ip address */ public @Nullable String getAddress() { - QbusConfiguration bridgeConfig = this.config; - if (bridgeConfig != null) { - if (bridgeConfig.addr != null) { - return bridgeConfig.addr; - } else { - return ""; - } + QbusConfiguration localConfig = this.bridgeConfig; + + if (localConfig != null) { + return localConfig.addr; } else { return null; } @@ -284,13 +335,10 @@ protected void readConfig() { * @return */ public @Nullable Integer getPort() { - QbusConfiguration bridgeConfig = this.config; - if (bridgeConfig != null) { - if (bridgeConfig.port != null) { - return bridgeConfig.port; - } else { - return 0; - } + QbusConfiguration localConfig = this.bridgeConfig; + + if (localConfig != null) { + return localConfig.port; } else { return null; } @@ -302,13 +350,10 @@ protected void readConfig() { * @return the serial nr of the controller */ public @Nullable String getSn() { - QbusConfiguration bridgeConfig = this.config; - if (bridgeConfig != null) { - if (bridgeConfig.sn != null) { - return bridgeConfig.sn; - } else { - return ""; - } + QbusConfiguration localConfig = this.bridgeConfig; + + if (localConfig != null) { + return localConfig.sn; } else { return null; } @@ -319,14 +364,11 @@ protected void readConfig() { * * @return the refresh interval */ - public @Nullable Integer getRefresh() { - QbusConfiguration bridgeConfig = this.config; - if (bridgeConfig != null) { - if (bridgeConfig.refresh != null) { - return bridgeConfig.refresh; - } else { - return 0; - } + public @Nullable Integer getServerCheck() { + QbusConfiguration localConfig = this.bridgeConfig; + + if (localConfig != null) { + return localConfig.serverCheck; } else { return null; } @@ -335,26 +377,4 @@ protected void readConfig() { @Override public void handleCommand(ChannelUID channelUID, Command command) { } - - /** - * Get state of bistabiel. - * - * @return bistabiel state - */ - public @Nullable QbusCommunication getQbusCommunication() { - if (this.qbusComm != null) { - return this.qbusComm; - } else { - return null; - } - } - - /** - * Sets state of bistabiel. - * - * @param bistabiel state - */ - void setQbusCommunication(QbusCommunication comm) { - this.qbusComm = comm; - } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java index 533822033e644..6a68c94b10783 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusConfiguration.java @@ -27,5 +27,5 @@ public class QbusConfiguration { public @Nullable String addr; public @Nullable Integer port; public @Nullable String sn; - public @Nullable Integer refresh; + public @Nullable Integer serverCheck; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java index aa82f11fc3eea..76f0d79e9afa5 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusBistabielHandler.java @@ -15,8 +15,8 @@ import static org.openhab.binding.qbus.internal.QbusBindingConstants.CHANNEL_SWITCH; import static org.openhab.core.types.RefreshType.REFRESH; +import java.io.IOException; import java.util.Map; -import java.util.concurrent.ScheduledFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -29,6 +29,8 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link QbusBistabielHandler} is responsible for handling the Bistable outputs of Qbus @@ -39,14 +41,14 @@ @NonNullByDefault public class QbusBistabielHandler extends QbusGlobalHandler { - protected @Nullable QbusThingsConfig config; + private final Logger logger = LoggerFactory.getLogger(QbusBistabielHandler.class); + + protected @Nullable QbusThingsConfig bistabielConfig = new QbusThingsConfig(); private @Nullable Integer bistabielId; private @Nullable String sn; - private @Nullable ScheduledFuture pollingJob; - public QbusBistabielHandler(Thing thing) { super(thing); } @@ -57,89 +59,77 @@ public QbusBistabielHandler(Thing thing) { @Override public void initialize() { readConfig(); - bistabielId = getId(); - QbusCommunication qComm = getCommunication("Bistabiel", bistabielId); - if (qComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "No communication with Qbus Bridge!"); - return; - } - - QbusBridgeHandler qBridgeHandler = getBridgeHandler("Bistabiel", bistabielId); - if (qBridgeHandler == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "No communication with Qbus Bridge!"); - return; - } + this.bistabielId = getId(); setSN(); - Map bistabielComm = qComm.getBistabiel(); + scheduler.submit(() -> { + QbusCommunication controllerComm; - if (bistabielComm != null) { - QbusBistabiel qBistabiel = bistabielComm.get(bistabielId); - if (qBistabiel != null) { - qBistabiel.setThingHandler(this); - handleStateUpdate(qBistabiel); + if (this.bistabielId != null) { + controllerComm = getCommunication("Bistabiel", this.bistabielId); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Error while initializing the thing."); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for BISTABIEL no set! " + this.bistabielId); + return; } - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Error while initializing the thing."); - } - } - /** - * Returns the serial number of the controller - * - * @return the serial nr - */ - public @Nullable String getSN() { - return this.sn; - } + if (controllerComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for BISTABIEL not known in controller " + this.bistabielId); + return; + } - /** - * Sets the serial number of the controller - */ - public void setSN() { - QbusBridgeHandler qBridgeHandler = getBridgeHandler("Bistabiel", bistabielId); - if (qBridgeHandler == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "No communication with Qbus Bridge!"); - return; - } - this.sn = qBridgeHandler.getSn(); + Map bistabielCommLocal = controllerComm.getBistabiel(); + + QbusBistabiel outputLocal = bistabielCommLocal.get(this.bistabielId); + + if (outputLocal == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge could not initialize BISTABIEL ID " + this.bistabielId); + return; + } + + outputLocal.setThingHandler(this); + handleStateUpdate(outputLocal); + + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Bistabiel", this.bistabielId); + + if (qBridgeHandler != null) { + if (qBridgeHandler.getStatus() == ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, + "Bridge offline for BISTABIEL ID " + this.bistabielId); + } + } + }); } /** - * Handle the status update from the thing + * Handle the status update from the bistabiel */ @Override public void handleCommand(ChannelUID channelUID, Command command) { - QbusCommunication qComm = getCommunication("Bistabiel", bistabielId); + QbusCommunication qComm = getCommunication("Bistabiel", this.bistabielId); + if (qComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for bistabiel " + bistabielId); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for BISTABIEL not known in controller " + this.bistabielId); return; - } - - Map bistabielComm = qComm.getBistabiel(); + } else { + Map bistabielComm = qComm.getBistabiel(); - if (bistabielComm != null) { - QbusBistabiel qBistabiel = bistabielComm.get(bistabielId); + QbusBistabiel qBistabiel = bistabielComm.get(this.bistabielId); if (qBistabiel == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "bridge communication not initialized when trying to execute command for bistabiel " - + bistabielId); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for BISTABIEL not known in controller " + this.bistabielId); return; } else { scheduler.submit(() -> { if (!qComm.communicationActive()) { - restartCommunication(qComm, "Bistabiel", bistabielId); + restartCommunication(qComm, "Bistabiel", this.bistabielId); } if (qComm.communicationActive()) { @@ -148,78 +138,107 @@ public void handleCommand(ChannelUID channelUID, Command command) { return; } - handleSwitchCommand(qBistabiel, channelUID, command); + switch (channelUID.getId()) { + case CHANNEL_SWITCH: + try { + handleSwitchCommand(qBistabiel, command); + } catch (IOException e) { + String message = e.getMessage(); + logger.warn("Error on executing Switch for bistabiel ID {}. IOException: {}", + this.bistabielId, message); + } catch (InterruptedException e) { + String message = e.getMessage(); + logger.warn( + "Error on executing Switch for bistabiel ID {}. Interruptedexception {}", + this.bistabielId, message); + } + break; + + default: + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Unknown Channel " + channelUID.getId()); + } } }); } - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Error while initializing the thing."); } } /** * Executes the switch command * + * @throws IOException * @throws InterruptedException */ - private void handleSwitchCommand(QbusBistabiel qBistabiel, ChannelUID channelUID, Command command) { - if (command instanceof OnOffType) { - String snr = getSN(); - if (snr != null) { + private void handleSwitchCommand(QbusBistabiel qBistabiel, Command command) + throws InterruptedException, IOException { + String snr = getSN(); + if (snr != null) { + if (command instanceof OnOffType) { if (command == OnOffType.OFF) { qBistabiel.execute(0, snr); } else { qBistabiel.execute(100, snr); } } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "No serial number configured for " + bistabielId); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "No serial number configured for BISTABIEL " + this.bistabielId); } } } /** * Method to update state of channel, called from Qbus Bistabiel. + * + * @param qBistabiel */ public void handleStateUpdate(QbusBistabiel qBistabiel) { Integer bistabielState = qBistabiel.getState(); if (bistabielState != null) { updateState(CHANNEL_SWITCH, (bistabielState == 0) ? OnOffType.OFF : OnOffType.ON); - updateStatus(ThingStatus.ONLINE); } } /** - * Read the configuration + * Returns the serial number of the controller + * + * @return the serial nr */ - protected synchronized void readConfig() { - final ScheduledFuture localPollingJob = this.pollingJob; + public @Nullable String getSN() { + return sn; + } - if (localPollingJob != null) { - localPollingJob.cancel(true); + /** + * Sets the serial number of the controller + */ + public void setSN() { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Bistabiel", this.bistabielId); + if (qBridgeHandler == null) { + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "No communication with Qbus Bridge for BISTABIEL " + this.bistabielId); + return; } + sn = qBridgeHandler.getSn(); + } - if (localPollingJob == null || localPollingJob.isCancelled()) { - this.config = getConfig().as(QbusThingsConfig.class); - } + /** + * Read the configuration + */ + protected synchronized void readConfig() { + bistabielConfig = getConfig().as(QbusThingsConfig.class); } /** * Returns the Id from the configuration * - * @return + * @return outputId */ public @Nullable Integer getId() { - QbusThingsConfig bistabielConfig = this.config; - if (bistabielConfig != null) { - if (bistabielConfig.bistabielId != null) { - return bistabielConfig.bistabielId; - } else { - return 0; - } + QbusThingsConfig localConfig = bistabielConfig; + if (localConfig != null) { + return localConfig.bistabielId; } else { - return 0; + return null; } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java index c38a22876752c..5b191b4e8557e 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusCO2Handler.java @@ -16,7 +16,6 @@ import static org.openhab.core.types.RefreshType.REFRESH; import java.util.Map; -import java.util.concurrent.ScheduledFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -41,12 +40,12 @@ public class QbusCO2Handler extends QbusGlobalHandler { protected @Nullable QbusThingsConfig config; + protected @Nullable QbusThingsConfig co2Config = new QbusThingsConfig(); + private @Nullable Integer co2Id; private @Nullable String sn; - private @Nullable ScheduledFuture pollingJob; - public QbusCO2Handler(Thing thing) { super(thing); } @@ -56,38 +55,50 @@ public QbusCO2Handler(Thing thing) { */ @Override public void initialize() { - setConfig(); - co2Id = getId(); + readConfig(); - QbusCommunication qComm = getCommunication("CO2", co2Id); - if (qComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "No communication with Qbus Bridge!"); - return; - } - - QbusBridgeHandler qBridgeHandler = getBridgeHandler("CO2", co2Id); - if (qBridgeHandler == null) { - return; - } + this.co2Id = getId(); setSN(); - Map co2Comm = qComm.getCo2(); + scheduler.submit(() -> { + QbusCommunication controllerComm; - if (co2Comm != null) { - QbusCO2 qCo2 = co2Comm.get(co2Id); - if (qCo2 != null) { - qCo2.setThingHandler(this); - handleStateUpdate(qCo2); + if (this.co2Id != null) { + controllerComm = getCommunication("CO2", this.co2Id); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Error while initializing the thing."); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for CO2 no set! " + this.co2Id); + return; } - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Error while initializing the thing."); - } + + if (controllerComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for CO2 not known in controller " + this.co2Id); + return; + } + + Map co2CommLocal = controllerComm.getCo2(); + + QbusCO2 outputLocal = co2CommLocal.get(this.co2Id); + + if (outputLocal == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "Bridge could not initialize CO2 ID " + this.co2Id); + return; + } + + outputLocal.setThingHandler(this); + handleStateUpdate(outputLocal); + + QbusBridgeHandler qBridgeHandler = getBridgeHandler("CO2", this.co2Id); + + if (qBridgeHandler != null) { + if (qBridgeHandler.getStatus() == ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, + "Bridge offline for CO2 ID " + this.co2Id); + } + } + }); } /** @@ -95,25 +106,23 @@ public void initialize() { */ @Override public void handleCommand(ChannelUID channelUID, Command command) { - QbusCommunication qComm = getCommunication("CO2", co2Id); + QbusCommunication qComm = getCommunication("CO2", this.co2Id); + if (qComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for CO2 " + co2Id); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for CO2 not known in controller " + this.co2Id); return; - } + } else { + Map co2Comm = qComm.getCo2(); - Map co2Comm = qComm.getCo2(); + QbusCO2 qCo2 = co2Comm.get(this.co2Id); - if (co2Comm != null) { - QbusCO2 qCo2 = co2Comm.get(co2Id); if (qCo2 == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for CO2 " + co2Id); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for CO2 not known in controller " + this.co2Id); return; } else { scheduler.submit(() -> { if (!qComm.communicationActive()) { - restartCommunication(qComm, "CO2", co2Id); + restartCommunication(qComm, "CO2", this.co2Id); } if (qComm.communicationActive()) { @@ -121,8 +130,13 @@ public void handleCommand(ChannelUID channelUID, Command command) { handleStateUpdate(qCo2); return; } - } + switch (channelUID.getId()) { + default: + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Unknown Channel " + channelUID.getId()); + } + } }); } } @@ -135,7 +149,6 @@ public void handleStateUpdate(QbusCO2 qCo2) { Integer co2State = qCo2.getState(); if (co2State != null) { updateState(CHANNEL_CO2, new DecimalType(co2State)); - updateStatus(ThingStatus.ONLINE); } } @@ -145,52 +158,40 @@ public void handleStateUpdate(QbusCO2 qCo2) { * @return the serial nr */ public @Nullable String getSN() { - return this.sn; + return sn; } /** * Sets the serial number of the controller */ public void setSN() { - QbusBridgeHandler qBridgeHandler = getBridgeHandler("CO2", co2Id); + QbusBridgeHandler qBridgeHandler = getBridgeHandler("CO2", this.co2Id); if (qBridgeHandler == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "No communication with Qbus Bridge!"); + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "No communication with Qbus Bridge for CO2 " + this.co2Id); return; } - this.sn = qBridgeHandler.getSn(); + sn = qBridgeHandler.getSn(); } /** * Read the configuration */ - protected synchronized void setConfig() { - final ScheduledFuture localPollingJob = this.pollingJob; - - if (localPollingJob != null) { - localPollingJob.cancel(true); - } - - if (localPollingJob == null || localPollingJob.isCancelled()) { - this.config = getConfig().as(QbusThingsConfig.class); - } + protected synchronized void readConfig() { + co2Config = getConfig().as(QbusThingsConfig.class); } /** * Returns the Id from the configuration * - * @return co2Id + * @return outputId */ public @Nullable Integer getId() { - QbusThingsConfig co2Config = this.config; - if (co2Config != null) { - if (co2Config.co2Id != null) { - return co2Config.co2Id; - } else { - return 0; - } + QbusThingsConfig localConfig = this.co2Config; + if (localConfig != null) { + return localConfig.co2Id; } else { - return 0; + return null; } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java index e5031105bd435..fc91c6a3f2984 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusDimmerHandler.java @@ -15,8 +15,8 @@ import static org.openhab.binding.qbus.internal.QbusBindingConstants.*; import static org.openhab.core.types.RefreshType.REFRESH; +import java.io.IOException; import java.util.Map; -import java.util.concurrent.ScheduledFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -31,6 +31,8 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link QbusDimmerHandler} is responsible for handling the dimmable outputs of Qbus @@ -41,14 +43,14 @@ @NonNullByDefault public class QbusDimmerHandler extends QbusGlobalHandler { - protected @Nullable QbusThingsConfig config; + private final Logger logger = LoggerFactory.getLogger(QbusDimmerHandler.class); + + protected @Nullable QbusThingsConfig dimmerConfig = new QbusThingsConfig(); private @Nullable Integer dimmerId; private @Nullable String sn; - private @Nullable ScheduledFuture pollingJob; - public QbusDimmerHandler(Thing thing) { super(thing); } @@ -58,87 +60,78 @@ public QbusDimmerHandler(Thing thing) { */ @Override public void initialize() { - setConfig(); - dimmerId = getId(); - - QbusCommunication qComm = getCommunication("Dimmer", dimmerId); - if (qComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "No communication with Qbus Bridge!"); - return; - } + readConfig(); - QbusBridgeHandler qBridgeHandler = getBridgeHandler("Dimmer", dimmerId); - if (qBridgeHandler == null) { - return; - } + this.dimmerId = getId(); setSN(); - Map dimmerComm = qComm.getDimmer(); - if (dimmerComm != null) { - QbusDimmer qDimmer = dimmerComm.get(dimmerId); - if (qDimmer != null) { - qDimmer.setThingHandler(this); - handleStateUpdate(qDimmer); + scheduler.submit(() -> { + QbusCommunication controllerComm; + + if (this.dimmerId != null) { + controllerComm = getCommunication("Dimmer", this.dimmerId); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Error while initializing the thing."); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for DIMMER no set! " + this.dimmerId); + return; } - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Error while initializing the thing."); - } - } - /** - * Returns the serial number of the controller - * - * @return the serial nr - */ - public @Nullable String getSN() { - return this.sn; - } + if (controllerComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for DIMMER not known in controller " + this.dimmerId); + return; + } - /** - * Sets the serial number of the controller - */ - public void setSN() { - QbusBridgeHandler qBridgeHandler = getBridgeHandler("Dimmer", dimmerId); - if (qBridgeHandler == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "No communication with Qbus Bridge!"); - return; - } else { - this.sn = qBridgeHandler.getSn(); - } + Map dimmerCommLocal = controllerComm.getDimmer(); + + QbusDimmer outputLocal = dimmerCommLocal.get(this.dimmerId); + + if (outputLocal == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge could not initialize DIMMER ID " + this.dimmerId); + return; + } + + outputLocal.setThingHandler(this); + handleStateUpdate(outputLocal); + + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Dimmer", this.dimmerId); + + if (qBridgeHandler != null) { + if (qBridgeHandler.getStatus() == ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, + "Bridge offline for DIMMER ID " + this.dimmerId); + } + } + }); } /** - * Handle the status update from the thing + * Handle the status update from the dimmer */ @Override public void handleCommand(ChannelUID channelUID, Command command) { - QbusCommunication qComm = getCommunication("Dimmer", dimmerId); + QbusCommunication qComm = getCommunication("Dimmer", this.dimmerId); if (qComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for dimmer " + dimmerId); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for DIMMER not known in controller " + this.dimmerId); return; - } + } else { + Map dimmerComm = qComm.getDimmer(); - Map dimmerComm = qComm.getDimmer(); - if (dimmerComm != null) { - QbusDimmer qDimmer = dimmerComm.get(dimmerId); + QbusDimmer qDimmer = dimmerComm.get(this.dimmerId); if (qDimmer == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for dimmer " + dimmerId); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for DIMMER not known in controller " + this.dimmerId); return; } else { scheduler.submit(() -> { if (!qComm.communicationActive()) { - restartCommunication(qComm, "Dimmer", dimmerId); + restartCommunication(qComm, "Dimmer", this.dimmerId); } if (qComm.communicationActive()) { @@ -149,113 +142,112 @@ public void handleCommand(ChannelUID channelUID, Command command) { switch (channelUID.getId()) { case CHANNEL_SWITCH: - handleSwitchCommand(qDimmer, command); + try { + handleSwitchCommand(qDimmer, command); + } catch (IOException e) { + String message = e.getMessage(); + logger.warn("Error on executing Switch for dimmer ID {}. IOException: {}", + this.dimmerId, message); + } catch (InterruptedException e) { + String message = e.getMessage(); + logger.warn("Error on executing Switch for dimmer ID {}. Interruptedexception {}", + this.dimmerId, message); + } break; case CHANNEL_BRIGHTNESS: - handleBrightnessCommand(qDimmer, command); + try { + handleBrightnessCommand(qDimmer, command); + } catch (IOException e) { + String message = e.getMessage(); + logger.warn("Error on executing Brightness for dimmer ID {}. IOException: {}", + this.dimmerId, message); + } catch (InterruptedException e) { + String message = e.getMessage(); + logger.warn( + "Error on executing Brightness for dimmer ID {}. Interruptedexception {}", + this.dimmerId, message); + } break; + + default: + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Unknown Channel " + channelUID.getId()); } } }); } - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Error while initializing the thing."); } } - /** - * - * @param message - */ - public void thingOffline(String message) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); - } - /** * Executes the switch command + * + * @throws IOException + * @throws InterruptedException */ - private void handleSwitchCommand(QbusDimmer qDimmer, Command command) { + private void handleSwitchCommand(QbusDimmer qDimmer, Command command) throws InterruptedException, IOException { if (command instanceof OnOffType) { String snr = getSN(); - - if (command == OnOffType.OFF) { - if (snr != null) { + if (snr != null) { + if (command == OnOffType.OFF) { qDimmer.execute(0, snr); } else { - thingOffline("No serial number configured for " + dimmerId); - } - } else { - if (snr != null) { qDimmer.execute(1000, snr); - } else { - thingOffline("No serial number configured for " + dimmerId); } + } else { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "No serial number configured for DIMMER " + this.dimmerId); } } } /** * Executes the brightness command + * + * @throws IOException + * @throws InterruptedException */ - private void handleBrightnessCommand(QbusDimmer qDimmer, Command command) { + private void handleBrightnessCommand(QbusDimmer qDimmer, Command command) throws InterruptedException, IOException { String snr = getSN(); - if (command instanceof OnOffType) { - if (command == OnOffType.OFF) { - if (snr != null) { + + if (snr == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "No serial number configured for DIMMER " + this.dimmerId); + return; + } else { + if (command instanceof OnOffType) { + if (command == OnOffType.OFF) { qDimmer.execute(0, snr); } else { - thingOffline("No serial number configured for " + dimmerId); - } - } else { - if (snr != null) { qDimmer.execute(100, snr); - } else { - thingOffline("No serial number configured for " + dimmerId); } - } - } else if (command instanceof IncreaseDecreaseType) { - int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); - Integer currentValue = qDimmer.getState(); - Integer newValue; - Integer sendvalue; - if (currentValue != null) { - if (command == IncreaseDecreaseType.INCREASE) { - newValue = currentValue + stepValue; - // round down to step multiple - newValue = newValue - newValue % stepValue; - sendvalue = newValue > 100 ? 100 : newValue; - if (snr != null) { + } else if (command instanceof IncreaseDecreaseType) { + int stepValue = ((Number) getConfig().get(CONFIG_STEP_VALUE)).intValue(); + Integer currentValue = qDimmer.getState(); + Integer newValue; + Integer sendvalue; + if (currentValue != null) { + if (command == IncreaseDecreaseType.INCREASE) { + newValue = currentValue + stepValue; + // round down to step multiple + newValue = newValue - newValue % stepValue; + sendvalue = newValue > 100 ? 100 : newValue; qDimmer.execute(sendvalue, snr); } else { - thingOffline("No serial number configured for " + dimmerId); - } - } else { - newValue = currentValue - stepValue; - // round up to step multiple - newValue = newValue + newValue % stepValue; - sendvalue = newValue < 0 ? 0 : newValue; - if (snr != null) { + newValue = currentValue - stepValue; + // round up to step multiple + newValue = newValue + newValue % stepValue; + sendvalue = newValue < 0 ? 0 : newValue; qDimmer.execute(sendvalue, snr); - } else { - thingOffline("No serial number configured for " + dimmerId); } } - } - } else if (command instanceof PercentType) { - int percentToInt = ((PercentType) command).intValue(); - if (command == PercentType.ZERO) { - if (snr != null) { + } else if (command instanceof PercentType) { + int percentToInt = ((PercentType) command).intValue(); + if (command == PercentType.ZERO) { qDimmer.execute(0, snr); } else { - thingOffline("No serial number configured for " + dimmerId); - } - } else { - if (snr != null) { qDimmer.execute(percentToInt, snr); - } else { - thingOffline("No serial number configured for " + dimmerId); } } } @@ -263,46 +255,56 @@ private void handleBrightnessCommand(QbusDimmer qDimmer, Command command) { /** * Method to update state of channel, called from Qbus Dimmer. + * + * @param qDimmer */ public void handleStateUpdate(QbusDimmer qDimmer) { Integer dimmerState = qDimmer.getState(); if (dimmerState != null) { updateState(CHANNEL_BRIGHTNESS, new PercentType(dimmerState)); - updateStatus(ThingStatus.ONLINE); } } /** - * Read the configuration + * Returns the serial number of the controller + * + * @return the serial number */ - protected synchronized void setConfig() { - final ScheduledFuture localPollingJob = this.pollingJob; + public @Nullable String getSN() { + return sn; + } - if (localPollingJob != null) { - localPollingJob.cancel(true); + /** + * Sets the serial number of the controller + */ + public void setSN() { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Dimmer", this.dimmerId); + if (qBridgeHandler == null) { + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "No communication with Qbus Bridge for DIMMER " + this.dimmerId); + return; } + this.sn = qBridgeHandler.getSn(); + } - if (localPollingJob == null || localPollingJob.isCancelled()) { - this.config = getConfig().as(QbusThingsConfig.class); - } - this.config = getConfig().as(QbusThingsConfig.class); + /** + * Read the configuration + */ + protected synchronized void readConfig() { + dimmerConfig = getConfig().as(QbusThingsConfig.class); } /** * Returns the Id from the configuration * - * @return dimmerId + * @return outputId */ public @Nullable Integer getId() { - QbusThingsConfig dimmerConfig = this.config; - if (dimmerConfig != null) { - if (dimmerConfig.dimmerId != null) { - return dimmerConfig.dimmerId; - } else { - return 0; - } + QbusThingsConfig localConfig = dimmerConfig; + if (localConfig != null) { + return localConfig.dimmerId; } else { - return 0; + return null; } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java index 7a9ee95bcf180..068813c899b5d 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusGlobalHandler.java @@ -23,8 +23,6 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseThingHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusGlobalHandler} is used in other handlers, to share the functions. @@ -35,8 +33,6 @@ @NonNullByDefault public abstract class QbusGlobalHandler extends BaseThingHandler { - private final Logger logger = LoggerFactory.getLogger(QbusGlobalHandler.class); - public QbusGlobalHandler(Thing thing) { super(thing); } @@ -92,20 +88,27 @@ public void restartCommunication(QbusCommunication qComm, String type, @Nullable qComm.restartCommunication(); } catch (InterruptedException e) { String message = e.toString(); - logger.debug("Error on stopping communication.{} ", message); + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message); } catch (IOException e) { String message = e.toString(); - logger.debug("Error on stopping communication.{} ", message); - } - - if (!qComm.communicationActive()) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Communication socket error"); - return; + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, message); } QbusBridgeHandler qBridgeHandler = getBridgeHandler(type, globalId); - if (qBridgeHandler != null) { + + if (qBridgeHandler != null && qComm.communicationActive()) { qBridgeHandler.bridgeOnline(); + } else { + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "Communication socket error"); } } + + /** + * Put thing offline + * + * @param message + */ + public void thingOffline(ThingStatusDetail detail, String message) { + updateStatus(ThingStatus.OFFLINE, detail, message); + } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java index 19eeebe9e3a37..0a1a532f11347 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusRolHandler.java @@ -16,8 +16,8 @@ import static org.openhab.core.library.types.UpDownType.DOWN; import static org.openhab.core.types.RefreshType.REFRESH; +import java.io.IOException; import java.util.Map; -import java.util.concurrent.ScheduledFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -32,6 +32,8 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link QbusRolHandler} is responsible for handling commands, which are @@ -43,14 +45,14 @@ @NonNullByDefault public class QbusRolHandler extends QbusGlobalHandler { - protected @Nullable QbusThingsConfig config; + private final Logger logger = LoggerFactory.getLogger(QbusRolHandler.class); + + protected @Nullable QbusThingsConfig rolConfig = new QbusThingsConfig(); private @Nullable Integer rolId; private @Nullable String sn; - private @Nullable ScheduledFuture pollingJob; - public QbusRolHandler(Thing thing) { super(thing); } @@ -60,60 +62,52 @@ public QbusRolHandler(Thing thing) { */ @Override public void initialize() { - setConfig(); - rolId = getId(); - - QbusCommunication qComm = getCommunication("Screen/Store", rolId); - if (qComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "No communication with Qbus Bridge!"); - return; - } + readConfig(); - QbusBridgeHandler qBridgeHandler = getBridgeHandler("Screen/Store", rolId); - if (qBridgeHandler == null) { - return; - } + this.rolId = getId(); setSN(); - Map rolComm = qComm.getRol(); + scheduler.submit(() -> { + QbusCommunication controllerComm; - if (rolComm != null) { - QbusRol qRol = rolComm.get(rolId); - if (qRol != null) { - qRol.setThingHandler(this); - handleStateUpdate(qRol); + if (this.rolId != null) { + controllerComm = getCommunication("Screen/Store", this.rolId); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Error while initializing the thing."); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for Screen/Store no set! " + this.rolId); + return; } - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Error while initializing the thing."); - } - } - /** - * Returns the serial number of the controller - * - * @return the serial nr - */ - public @Nullable String getSN() { - return this.sn; - } + if (controllerComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for Screen/Store not known in controller " + this.rolId); + return; + } - /** - * Sets the serial number of the controller - */ - public void setSN() { - QbusBridgeHandler qBridgeHandler = getBridgeHandler("Screen/Store", rolId); - if (qBridgeHandler == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "No communication with Qbus Bridge!"); - return; - } - this.sn = qBridgeHandler.getSn(); + Map rolCommLocal = controllerComm.getRol(); + + QbusRol outputLocal = rolCommLocal.get(this.rolId); + + if (outputLocal == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge could not initialize Screen/Store ID " + this.rolId); + return; + } + + outputLocal.setThingHandler(this); + handleStateUpdate(outputLocal); + + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Screen/Store", this.rolId); + + if (qBridgeHandler != null) { + if (qBridgeHandler.getStatus() == ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, + "Bridge offline for SCREEN/STORE ID " + this.rolId); + } + } + }); } /** @@ -121,26 +115,25 @@ public void setSN() { */ @Override public void handleCommand(ChannelUID channelUID, Command command) { - QbusCommunication qComm = getCommunication("Screen/Store", rolId); + QbusCommunication qComm = getCommunication("Screen/Store", this.rolId); if (qComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for Screen/Store " + rolId); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for ROLLERSHUTTER/SCREEN not known in controller " + this.rolId); return; - } + } else { + Map rolComm = qComm.getRol(); - Map rolComm = qComm.getRol(); + QbusRol qRol = rolComm.get(this.rolId); - if (rolComm != null) { - QbusRol qRol = rolComm.get(rolId); if (qRol == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for ROL " + rolId); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for ROLLERSHUTTER/SCREEN not known in controller " + this.rolId); return; } else { scheduler.submit(() -> { if (!qComm.communicationActive()) { - restartCommunication(qComm, "Screen/Store", rolId); + restartCommunication(qComm, "Screen/Store", this.rolId); } if (qComm.communicationActive()) { @@ -151,11 +144,32 @@ public void handleCommand(ChannelUID channelUID, Command command) { switch (channelUID.getId()) { case CHANNEL_ROLLERSHUTTER: - handleScreenposCommand(qRol, command); + try { + handleScreenposCommand(qRol, command); + } catch (IOException e) { + String message = e.getMessage(); + logger.warn("Error on executing Rollershutter for screen ID {}. IOException: {}", + this.rolId, message); + } catch (InterruptedException e) { + String message = e.toString(); + logger.warn( + "Error on executing Rollershutter for screen ID {}. Interruptedexception {}", + this.rolId, message); + } break; case CHANNEL_SLATS: - handleSlatsposCommand(qRol, command); + try { + handleSlatsposCommand(qRol, command); + } catch (IOException e) { + String message = e.getMessage(); + logger.warn("Error on executing Slats for screen ID {}. IOException: {}", + this.rolId, message); + } catch (InterruptedException e) { + String message = e.toString(); + logger.warn("Error on executing Slats for screen ID {}. Interruptedexception {}", + this.rolId, message); + } break; } } @@ -164,77 +178,50 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - /** - * - * @param message - */ - public void thingOffline(String message) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); - } - /** * Executes the command for screen up/down position + * + * @throws IOException + * @throws InterruptedException */ - private void handleScreenposCommand(QbusRol qRol, Command command) { + private void handleScreenposCommand(QbusRol qRol, Command command) throws InterruptedException, IOException { String snr = getSN(); - if (command instanceof UpDownType) { - UpDownType upDown = (UpDownType) command; - if (upDown == DOWN) { - if (snr != null) { + if (snr != null) { + if (command instanceof UpDownType) { + UpDownType upDown = (UpDownType) command; + if (upDown == DOWN) { qRol.execute(0, snr); } else { - thingOffline("No serial number configured for " + rolId); - } - } else { - if (snr != null) { qRol.execute(100, snr); - } else { - thingOffline("No serial number configured for " + rolId); } - } - } else if (command instanceof IncreaseDecreaseType) { - IncreaseDecreaseType inc = (IncreaseDecreaseType) command; - int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); - Integer currentValue = qRol.getState(); - int newValue; - int sendValue; - if (currentValue != null) { - if (inc == IncreaseDecreaseType.INCREASE) { - newValue = currentValue + stepValue; - // round down to step multiple - newValue = newValue - newValue % stepValue; - sendValue = newValue > 100 ? 100 : newValue; - if (snr != null) { + } else if (command instanceof IncreaseDecreaseType) { + IncreaseDecreaseType inc = (IncreaseDecreaseType) command; + int stepValue = ((Number) getConfig().get(CONFIG_STEP_VALUE)).intValue(); + Integer currentValue = qRol.getState(); + int newValue; + int sendValue; + if (currentValue != null) { + if (inc == IncreaseDecreaseType.INCREASE) { + newValue = currentValue + stepValue; + // round down to step multiple + newValue = newValue - newValue % stepValue; + sendValue = newValue > 100 ? 100 : newValue; qRol.execute(sendValue, snr); } else { - thingOffline("No serial number configured for " + rolId); - } - } else { - newValue = currentValue - stepValue; - // round up to step multiple - newValue = newValue + newValue % stepValue; - sendValue = newValue > 100 ? 100 : newValue; - if (snr != null) { + newValue = currentValue - stepValue; + // round up to step multiple + newValue = newValue + newValue % stepValue; + sendValue = newValue > 100 ? 100 : newValue; qRol.execute(sendValue, snr); - } else { - thingOffline("No serial number configured for " + rolId); } } - } - } else if (command instanceof PercentType) { - PercentType p = (PercentType) command; - int pp = p.intValue(); - if (p == PercentType.ZERO) { - if (snr != null) { + } else if (command instanceof PercentType) { + PercentType p = (PercentType) command; + int pp = p.intValue(); + if (p == PercentType.ZERO) { qRol.execute(0, snr); } else { - thingOffline("No serial number configured for " + rolId); - } - } else { - if (snr != null) { qRol.execute(pp, snr); - } else { - thingOffline("No serial number configured for " + rolId); } } } @@ -242,64 +229,45 @@ private void handleScreenposCommand(QbusRol qRol, Command command) { /** * Executes the command for screen slats position + * + * @throws IOException + * @throws InterruptedException */ - private void handleSlatsposCommand(QbusRol qRol, Command command) { + private void handleSlatsposCommand(QbusRol qRol, Command command) throws InterruptedException, IOException { String snr = getSN(); - if (command instanceof UpDownType) { - if (command == DOWN) { - if (snr != null) { + if (snr != null) { + if (command instanceof UpDownType) { + if (command == DOWN) { qRol.executeSlats(0, snr); } else { - thingOffline("No serial number configured for " + rolId); - } - } else { - if (snr != null) { qRol.executeSlats(100, snr); - } else { - thingOffline("No serial number configured for " + rolId); } - } - } else if (command instanceof IncreaseDecreaseType) { - int stepValue = ((Number) this.getConfig().get(CONFIG_STEP_VALUE)).intValue(); - Integer currentValue = qRol.getState(); - int newValue; - int sendValue; - if (currentValue != null) { - if (command == IncreaseDecreaseType.INCREASE) { - newValue = currentValue + stepValue; - // round down to step multiple - newValue = newValue - newValue % stepValue; - sendValue = newValue > 100 ? 100 : newValue; - if (snr != null) { + } else if (command instanceof IncreaseDecreaseType) { + int stepValue = ((Number) getConfig().get(CONFIG_STEP_VALUE)).intValue(); + Integer currentValue = qRol.getState(); + int newValue; + int sendValue; + if (currentValue != null) { + if (command == IncreaseDecreaseType.INCREASE) { + newValue = currentValue + stepValue; + // round down to step multiple + newValue = newValue - newValue % stepValue; + sendValue = newValue > 100 ? 100 : newValue; qRol.executeSlats(sendValue, snr); } else { - thingOffline("No serial number configured for " + rolId); - } - } else { - newValue = currentValue - stepValue; - // round up to step multiple - newValue = newValue + newValue % stepValue; - sendValue = newValue > 100 ? 100 : newValue; - if (snr != null) { + newValue = currentValue - stepValue; + // round up to step multiple + newValue = newValue + newValue % stepValue; + sendValue = newValue > 100 ? 100 : newValue; qRol.executeSlats(sendValue, snr); - } else { - thingOffline("No serial number configured for " + rolId); } } - } - } else if (command instanceof PercentType) { - int percentToInt = ((PercentType) command).intValue(); - if (command == PercentType.ZERO) { - if (snr != null) { + } else if (command instanceof PercentType) { + int percentToInt = ((PercentType) command).intValue(); + if (command == PercentType.ZERO) { qRol.executeSlats(0, snr); } else { - thingOffline("No serial number configured for " + rolId); - } - } else { - if (snr != null) { qRol.executeSlats(percentToInt, snr); - } else { - thingOffline("No serial number configured for " + rolId); } } } @@ -314,45 +282,52 @@ public void handleStateUpdate(QbusRol qRol) { if (rolState != null) { updateState(CHANNEL_ROLLERSHUTTER, new PercentType(rolState)); - updateStatus(ThingStatus.ONLINE); } if (slatState != null) { updateState(CHANNEL_SLATS, new PercentType(slatState)); - updateStatus(ThingStatus.ONLINE); } } /** - * Read the configuration + * Returns the serial number of the controller + * + * @return the serial nr */ - protected synchronized void setConfig() { - final ScheduledFuture localPollingJob = this.pollingJob; + public @Nullable String getSN() { + return sn; + } - if (localPollingJob != null) { - localPollingJob.cancel(true); + /** + * Sets the serial number of the controller + */ + public void setSN() { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Screen/Store", this.rolId); + if (qBridgeHandler == null) { + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "No communication with Qbus Bridge for ROLLERSHUTTER/SCREEN " + this.rolId); + return; } + sn = qBridgeHandler.getSn(); + } - if (localPollingJob == null || localPollingJob.isCancelled()) { - this.config = getConfig().as(QbusThingsConfig.class); - } - this.config = getConfig().as(QbusThingsConfig.class); + /** + * Read the configuration + */ + protected synchronized void readConfig() { + rolConfig = getConfig().as(QbusThingsConfig.class); } /** * Returns the Id from the configuration * - * @return rolId + * @return outputId */ public @Nullable Integer getId() { - QbusThingsConfig rolConfig = this.config; - if (rolConfig != null) { - if (rolConfig.rolId != null) { - return rolConfig.rolId; - } else { - return 0; - } + QbusThingsConfig localConfig = rolConfig; + if (localConfig != null) { + return localConfig.rolId; } else { - return 0; + return null; } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java index 88924a12b90cd..fc0381fd4af06 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusSceneHandler.java @@ -14,8 +14,8 @@ import static org.openhab.binding.qbus.internal.QbusBindingConstants.CHANNEL_SCENE; +import java.io.IOException; import java.util.Map; -import java.util.concurrent.ScheduledFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -28,6 +28,8 @@ import org.openhab.core.thing.ThingStatus; import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.types.Command; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * The {@link QbusSceneHandler} is responsible for handling commands, which are @@ -39,14 +41,14 @@ @NonNullByDefault public class QbusSceneHandler extends QbusGlobalHandler { - protected @Nullable QbusThingsConfig config; + private final Logger logger = LoggerFactory.getLogger(QbusSceneHandler.class); + + protected @Nullable QbusThingsConfig sceneConfig = new QbusThingsConfig(); private @Nullable Integer sceneId; private @Nullable String sn; - private @Nullable ScheduledFuture pollingJob; - public QbusSceneHandler(Thing thing) { super(thing); } @@ -56,60 +58,48 @@ public QbusSceneHandler(Thing thing) { */ @Override public void initialize() { - setConfig(); - sceneId = getId(); - - QbusCommunication qComm = getCommunication("Scene", sceneId); - if (qComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "No communication with Qbus Bridge!"); - return; - } + readConfig(); - QbusBridgeHandler qBridgeHandler = getBridgeHandler("Scene", sceneId); - if (qBridgeHandler == null) { - return; - } + this.sceneId = getId(); setSN(); - Map sceneComm = qComm.getScene(); + scheduler.submit(() -> { + QbusCommunication controllerComm; - if (sceneComm != null) { - QbusScene qScene = sceneComm.get(sceneId); - if (qScene != null) { - qScene.setThingHandler(this); - updateStatus(ThingStatus.ONLINE); + if (this.sceneId != null) { + controllerComm = getCommunication("Scene", this.sceneId); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Error while initializing the thing."); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for SCENE no set! " + this.sceneId); + return; } - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Error while initializing the thing."); - } - } - /** - * Returns the serial number of the controller - * - * @return the serial nr - */ - public @Nullable String getSN() { - return this.sn; - } + if (controllerComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for SCENE not known in controller " + this.sceneId); + return; + } - /** - * Sets the serial number of the controller - */ - public void setSN() { - QbusBridgeHandler qBridgeHandler = getBridgeHandler("Scene", sceneId); - if (qBridgeHandler == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "No communication with Qbus Bridge!"); - return; - } - this.sn = qBridgeHandler.getSn(); + Map sceneCommLocal = controllerComm.getScene(); + + QbusScene outputLocal = sceneCommLocal.get(this.sceneId); + + if (outputLocal == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge could not initialize SCENE ID " + this.sceneId); + return; + } + + outputLocal.setThingHandler(this); + + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Scene", this.sceneId); + + if ((qBridgeHandler != null) && (qBridgeHandler.getStatus() == ThingStatus.ONLINE)) { + updateStatus(ThingStatus.ONLINE); + } else { + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, "Bridge offline for SCENE ID " + this.sceneId); + } + }); } /** @@ -117,33 +107,44 @@ public void setSN() { */ @Override public void handleCommand(ChannelUID channelUID, Command command) { - QbusCommunication qComm = getCommunication("Scene", sceneId); + QbusCommunication qComm = getCommunication("Scene", this.sceneId); if (qComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for Scene " + sceneId); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for SCENE not known in controller " + this.sceneId); return; - } - - Map sceneComm = qComm.getScene(); + } else { + Map sceneComm = qComm.getScene(); + QbusScene qScene = sceneComm.get(this.sceneId); - if (sceneComm != null) { - QbusScene qScene = sceneComm.get(sceneId); if (qScene == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for Scene " + sceneId); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for SCENE not known in controller " + this.sceneId); return; } else { scheduler.submit(() -> { if (!qComm.communicationActive()) { - restartCommunication(qComm, "Scene", sceneId); + restartCommunication(qComm, "Scene", this.sceneId); } if (qComm.communicationActive()) { switch (channelUID.getId()) { case CHANNEL_SCENE: - handleSwitchCommand(qScene, channelUID, command); + try { + handleSwitchCommand(qScene, channelUID, command); + } catch (IOException e) { + String message = e.getMessage(); + logger.warn("Error on executing Scene for scene ID {}. IOException: {}", + this.sceneId, message); + } catch (InterruptedException e) { + String message = e.getMessage(); + logger.warn("Error on executing Scene for scene ID {}. Interruptedexception {}", + this.sceneId, message); + } break; + + default: + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Unknown Channel " + channelUID.getId()); } } }); @@ -152,77 +153,65 @@ public void handleCommand(ChannelUID channelUID, Command command) { } /** - * Method to update state of channel, called from Qbus Scene. + * Executes the scene command + * + * @throws IOException + * @throws InterruptedException */ - public void handleStateUpdate(QbusScene qScene) { - Integer sceneState = qScene.getState(); - if (sceneState != null) { - updateState(CHANNEL_SCENE, (sceneState == 0) ? OnOffType.OFF : OnOffType.ON); - updateStatus(ThingStatus.ONLINE); + void handleSwitchCommand(QbusScene qScene, ChannelUID channelUID, Command command) + throws InterruptedException, IOException { + String snr = getSN(); + if (snr != null) { + if (command instanceof OnOffType) { + if (command == OnOffType.OFF) { + qScene.execute(0, snr); + } else { + qScene.execute(100, snr); + } + } } } /** + * Returns the serial number of the controller * - * @param message + * @return the serial nr */ - public void thingOffline(String message) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); + public @Nullable String getSN() { + return sn; } /** - * Executes the scene command + * Sets the serial number of the controller */ - void handleSwitchCommand(QbusScene qScene, ChannelUID channelUID, Command command) { - String snr = getSN(); - if (command instanceof OnOffType) { - if (command == OnOffType.OFF) { - if (snr != null) { - qScene.execute(0, snr); - } else { - thingOffline("No serial number configured for " + sceneId); - } - } else { - if (snr != null) { - qScene.execute(100, snr); - } else { - thingOffline("No serial number configured for " + sceneId); - } - } + public void setSN() { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Scene", this.sceneId); + if (qBridgeHandler == null) { + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "No communication with Qbus Bridge for SCENE " + this.sceneId); + return; } + sn = qBridgeHandler.getSn(); } /** * Read the configuration */ - protected synchronized void setConfig() { - final ScheduledFuture localPollingJob = this.pollingJob; - - if (localPollingJob != null) { - localPollingJob.cancel(true); - } - - if (localPollingJob == null || localPollingJob.isCancelled()) { - this.config = getConfig().as(QbusThingsConfig.class); - } - this.config = getConfig().as(QbusThingsConfig.class); + protected synchronized void readConfig() { + sceneConfig = getConfig().as(QbusThingsConfig.class); } /** * Returns the Id from the configuration * - * @return sceneId + * @return outputId */ public @Nullable Integer getId() { - QbusThingsConfig sceneConfig = this.config; - if (sceneConfig != null) { - if (sceneConfig.sceneId != null) { - return sceneConfig.sceneId; - } else { - return 0; - } + QbusThingsConfig localConfig = sceneConfig; + if (localConfig != null) { + return localConfig.sceneId; } else { - return 0; + return null; } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java index 0d90eb9b34a12..cbef42e775eb3 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/handler/QbusThermostatHandler.java @@ -16,8 +16,8 @@ import static org.openhab.core.library.unit.SIUnits.CELSIUS; import static org.openhab.core.types.RefreshType.REFRESH; +import java.io.IOException; import java.util.Map; -import java.util.concurrent.ScheduledFuture; import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; @@ -35,8 +35,7 @@ import org.slf4j.LoggerFactory; /** - * The {@link QbusThermostatHandler} is responsible for handling commands, which are - * sent to one of the channels. + * The {@link QbusThermostatHandler} is responsible for handling the Thermostat outputs of Qbus * * @author Koen Schockaert - Initial Contribution */ @@ -46,14 +45,12 @@ public class QbusThermostatHandler extends QbusGlobalHandler { private final Logger logger = LoggerFactory.getLogger(QbusThermostatHandler.class); - protected @Nullable QbusThingsConfig config; + protected @Nullable QbusThingsConfig thermostatConfig = new QbusThingsConfig(); private @Nullable Integer thermostatId; private @Nullable String sn; - private @Nullable ScheduledFuture pollingJob; - public QbusThermostatHandler(Thing thing) { super(thing); } @@ -63,90 +60,78 @@ public QbusThermostatHandler(Thing thing) { */ @Override public void initialize() { - setConfig(); - thermostatId = getId(); - - QbusCommunication qComm = getCommunication("Thermostat", thermostatId); - if (qComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "No communication with Qbus Bridge!"); - return; - } + readConfig(); - QbusBridgeHandler qBridgeHandler = getBridgeHandler("Thermostat", thermostatId); - if (qBridgeHandler == null) { - return; - } + this.thermostatId = getId(); setSN(); - Map thermostatComm = qComm.getThermostat(); + scheduler.submit(() -> { + QbusCommunication controllerComm; - if (thermostatComm != null) { - QbusThermostat qThermostat = thermostatComm.get(thermostatId); - if (qThermostat != null) { - qThermostat.setThingHandler(this); - handleStateUpdate(qThermostat); + if (this.thermostatId != null) { + controllerComm = getCommunication("Thermostat", this.thermostatId); } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Error while initializing the thing."); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, "ID for THERMOSTAT no set! " + this.thermostatId); + return; } - } else { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Error while initializing the thing."); - } - } - /** - * Returns the serial number of the controller - * - * @return the serial nr - */ - public @Nullable String getSN() { - return this.sn; - } + if (controllerComm == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for THERMOSTAT not known in controller " + this.thermostatId); + return; + } - /** - * Sets the serial number of the controller - */ - public void setSN() { - QbusBridgeHandler qBridgeHandler = getBridgeHandler("Thermostat", thermostatId); - if (qBridgeHandler == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "No communication with Qbus Bridge!"); - return; - } - this.sn = qBridgeHandler.getSn(); + Map thermostatlCommLocal = controllerComm.getThermostat(); + + QbusThermostat outputLocal = thermostatlCommLocal.get(this.thermostatId); + + if (outputLocal == null) { + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "Bridge could not initialize THERMOSTAT ID " + this.thermostatId); + return; + } + + outputLocal.setThingHandler(this); + handleStateUpdate(outputLocal); + + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Thermostat", this.thermostatId); + + if (qBridgeHandler != null) { + if (qBridgeHandler.getStatus() == ThingStatus.ONLINE) { + updateStatus(ThingStatus.ONLINE); + } else { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE, + "Bridge offline for THERMOSTAT ID " + this.thermostatId); + } + } + }); } /** - * Handle the status update from the thing + * Handle the status update from the thermostat */ @Override public void handleCommand(ChannelUID channelUID, Command command) { - QbusCommunication qComm = getCommunication("Thermostat", thermostatId); + QbusCommunication qComm = getCommunication("Thermostat", this.thermostatId); if (qComm == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for thermostat " - + thermostatId); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for THERMOSTAT not known in controller " + this.thermostatId); return; - } - - Map thermostatComm = qComm.getThermostat(); + } else { + Map thermostatComm = qComm.getThermostat(); - if (thermostatComm != null) { - QbusThermostat qThermostat = thermostatComm.get(thermostatId); + QbusThermostat qThermostat = thermostatComm.get(this.thermostatId); if (qThermostat == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, - "Bridge communication not initialized when trying to execute command for Scene " - + thermostatId); + thingOffline(ThingStatusDetail.CONFIGURATION_ERROR, + "ID for THERMOSTAT not known in controller " + this.thermostatId); return; } else { scheduler.submit(() -> { if (!qComm.communicationActive()) { - restartCommunication(qComm, "Thermostat", thermostatId); + restartCommunication(qComm, "Thermostat", this.thermostatId); } if (qComm.communicationActive()) { @@ -157,13 +142,38 @@ public void handleCommand(ChannelUID channelUID, Command command) { switch (channelUID.getId()) { case CHANNEL_MODE: - handleModeCommand(qThermostat, command); + try { + handleModeCommand(qThermostat, command); + } catch (IOException e) { + String message = e.getMessage(); + logger.warn("Error on executing Mode for thermostat ID {}. IOException: {} ", + this.thermostatId, message); + } catch (InterruptedException e) { + String message = e.getMessage(); + logger.warn( + "Error on executing Mode for thermostat ID {}. Interruptedexception {} ", + this.thermostatId, message); + } break; case CHANNEL_SETPOINT: - handleSetpointCommand(qThermostat, command); + try { + handleSetpointCommand(qThermostat, command); + } catch (IOException e) { + String message = e.getMessage(); + logger.warn("Error on executing Setpoint for thermostat ID {}. IOException: {} ", + this.thermostatId, message); + } catch (InterruptedException e) { + String message = e.getMessage(); + logger.warn( + "Error on executing Setpoint for thermostat ID {}. Interruptedexception {} ", + this.thermostatId, message); + } break; + default: + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "Unknown Channel " + channelUID.getId()); } } }); @@ -171,48 +181,49 @@ public void handleCommand(ChannelUID channelUID, Command command) { } } - /** - * Puts thing offline - * - * @param message - */ - public void thingOffline(String message) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, message); - } - /** * Executes the Mode command + * + * @param qThermostat + * @param command + * @param snr + * @throws InterruptedException + * @throws IOException */ - private void handleModeCommand(QbusThermostat qThermostat, Command command) { + private void handleModeCommand(QbusThermostat qThermostat, Command command) + throws InterruptedException, IOException { String snr = getSN(); - if (command instanceof DecimalType) { - int mode = ((DecimalType) command).intValue(); - if (snr != null) { + if (snr != null) { + if (command instanceof DecimalType) { + int mode = ((DecimalType) command).intValue(); qThermostat.executeMode(mode, snr); - } else { - thingOffline("No serial number configured for " + thermostatId); } } } /** * Executes the Setpoint command + * + * @param qThermostat + * @param command + * @param snr + * @throws InterruptedException + * @throws IOException */ - private void handleSetpointCommand(QbusThermostat qThermostat, Command command) { + private void handleSetpointCommand(QbusThermostat qThermostat, Command command) + throws InterruptedException, IOException { String snr = getSN(); - if (command instanceof QuantityType) { - QuantityType s = (QuantityType) command; - double sp = s.doubleValue(); - QuantityType spCelcius = s.toUnit(CELSIUS); + if (snr != null) { + if (command instanceof QuantityType) { + QuantityType s = (QuantityType) command; + double sp = s.doubleValue(); + QuantityType spCelcius = s.toUnit(CELSIUS); - if (snr != null) { if (spCelcius != null) { qThermostat.executeSetpoint(sp, snr); } else { - logger.debug("Could not set setpoint for thermostat (conversion failed) {}", thermostatId); + logger.warn("Could not set setpoint for thermostat (conversion failed) {}", this.thermostatId); } - } else { - thingOffline("No serial number configured for " + thermostatId); } } } @@ -220,50 +231,65 @@ private void handleSetpointCommand(QbusThermostat qThermostat, Command command) /** * Method to update state of all channels, called from Qbus thermostat. * - * @param qThermostat Qbus thermostat - * + * @param qThermostat */ - public void handleStateUpdate(QbusThermostat qThermostat) { - updateState(CHANNEL_MEASURED, new QuantityType<>(qThermostat.getMeasured(), CELSIUS)); - - updateState(CHANNEL_SETPOINT, new QuantityType<>(qThermostat.getSetpoint(), CELSIUS)); + Double measured = qThermostat.getMeasured(); + if (measured != null) { + updateState(CHANNEL_MEASURED, new QuantityType<>(measured, CELSIUS)); + } - updateState(CHANNEL_MODE, new DecimalType(qThermostat.getMode())); + Double setpoint = qThermostat.getSetpoint(); + if (setpoint != null) { + updateState(CHANNEL_SETPOINT, new QuantityType<>(setpoint, CELSIUS)); + } - updateStatus(ThingStatus.ONLINE); + Integer mode = qThermostat.getMode(); + if (mode != null) { + updateState(CHANNEL_MODE, new DecimalType(mode)); + } } /** - * Read the configuration + * Returns the serial number of the controller + * + * @return the serial nr */ - protected synchronized void setConfig() { - final ScheduledFuture localPollingJob = this.pollingJob; + public @Nullable String getSN() { + return sn; + } - if (localPollingJob != null) { - localPollingJob.cancel(true); + /** + * Sets the serial number of the controller + */ + public void setSN() { + QbusBridgeHandler qBridgeHandler = getBridgeHandler("Thermostsat", this.thermostatId); + if (qBridgeHandler == null) { + thingOffline(ThingStatusDetail.COMMUNICATION_ERROR, + "No communication with Qbus Bridge for THERMOSTAT " + this.thermostatId); + return; } + sn = qBridgeHandler.getSn(); + } - if (localPollingJob == null || localPollingJob.isCancelled()) { - this.config = getConfig().as(QbusThingsConfig.class); - } + /** + * Read the configuration + */ + protected synchronized void readConfig() { + thermostatConfig = getConfig().as(QbusThingsConfig.class); } /** * Returns the Id from the configuration * - * @return thermostatId + * @return outputId */ public @Nullable Integer getId() { - QbusThingsConfig thConfig = this.config; - if (thConfig != null) { - if (thConfig.thermostatId != null) { - return thConfig.thermostatId; - } else { - return 0; - } + QbusThingsConfig localConfig = thermostatConfig; + if (localConfig != null) { + return localConfig.thermostatId; } else { - return 0; + return null; } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java index 531e8596571a0..a55ec87431c69 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusBistabiel.java @@ -12,11 +12,11 @@ */ package org.openhab.binding.qbus.internal.protocol; +import java.io.IOException; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusBistabielHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusBistabiel} class represents the Qbus BISTABIEL output. @@ -27,24 +27,22 @@ @NonNullByDefault public final class QbusBistabiel { - private final Logger logger = LoggerFactory.getLogger(QbusBistabiel.class); - private @Nullable QbusCommunication qComm; - private String id; + private Integer id; private @Nullable Integer state; private @Nullable QbusBistabielHandler thingHandler; - QbusBistabiel(String id) { + QbusBistabiel(Integer id) { this.id = id; } /** * This method should be called if the ThingHandler for the thing corresponding to this bistabiel is initialized. * It keeps a record of the thing handler in this object so the thing can be updated when - * the bistable output receives an update from the Qbus IP-interface. + * the bistable output receives an update from the Qbus client. * * @param handler */ @@ -55,7 +53,7 @@ public void setThingHandler(QbusBistabielHandler handler) { /** * This method sets a pointer to the qComm BISTABIEL of class {@link QbusCommuncation}. * This is then used to be able to call back the sendCommand method in this class to send a command to the - * Qbus IP-interface when.. + * Qbus client. * * @param qComm */ @@ -64,45 +62,40 @@ public void setQComm(QbusCommunication qComm) { } /** - * Get state of bistabiel. + * Update the value of the Bistabiel. * - * @return bistabiel state + * @param state */ - public @Nullable Integer getState() { - if (this.state != null) { - return this.state; - } else { - return null; + void updateState(@Nullable Integer state) { + this.state = state; + QbusBistabielHandler handler = this.thingHandler; + if (handler != null) { + handler.handleStateUpdate(this); } } /** - * Sets state of bistabiel. + * Get the value of the Bistabiel. * - * @param bistabiel state + * @return */ - void setState(int state) { - this.state = state; - QbusBistabielHandler handler = this.thingHandler; - if (handler != null) { - handler.handleStateUpdate(this); - } + public @Nullable Integer getState() { + return this.state; } /** - * Sends bistabiel to Qbus. + * Sends Bistabiel state to Qbus. * + * @param value + * @param sn * @throws InterruptedException + * @throws IOException */ - public void execute(int value, String sn) { + public void execute(int value, String sn) throws InterruptedException, IOException { QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeBistabiel").withId(this.id).withState(value); - QbusCommunication comm = qComm; + QbusCommunication comm = this.qComm; if (comm != null) { - try { - comm.sendMessage(qCmd); - } catch (InterruptedException e) { - logger.warn("Could not send command for bistabiel {}, {}", this.id, e.getMessage()); - } + comm.sendMessage(qCmd); } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java index 10c9a20f185c0..9c3276a31edb0 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java @@ -30,6 +30,9 @@ public final class QbusCO2 { private @Nullable QbusCO2Handler thingHandler; + QbusCO2() { + } + /** * This method should be called if the ThingHandler for the thing corresponding to this CO2 is initialized. * It keeps a record of the thing handler in this object so the thing can be updated when @@ -41,27 +44,34 @@ public void setThingHandler(QbusCO2Handler handler) { this.thingHandler = handler; } + /** + * This method sets a pointer to the qComm CO2 of class {@link QbusCommuncation}. + * This is then used to be able to call back the sendCommand method in this class to send a command to the + * Qbus client. + * + * @param qComm + */ + // public void setQComm(QbusCommunication qComm) { + // this.qComm = qComm; + // } + /** * Get state of CO2. * * @return CO2 state */ public @Nullable Integer getState() { - if (this.state != null) { - return this.state; - } else { - return null; - } + return this.state; } /** - * Sets state of CO2. + * Update the value of the CO2. * - * @param CO2 state + * @param CO2 value */ - public void setState(Integer co2) { - this.state = co2; - QbusCO2Handler handler = thingHandler; + void updateState(@Nullable Integer state) { + this.state = state; + QbusCO2Handler handler = this.thingHandler; if (handler != null) { handler.handleStateUpdate(this); } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java index 7e63b8ff6d8bf..6df9d47d87d1a 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java @@ -18,6 +18,7 @@ import java.io.PrintWriter; import java.net.InetAddress; import java.net.Socket; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -31,6 +32,7 @@ import org.openhab.core.common.NamedThreadFactory; import org.openhab.core.thing.ChannelUID; import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatusDetail; import org.openhab.core.thing.binding.BaseThingHandler; import org.openhab.core.types.Command; import org.slf4j.Logger; @@ -42,10 +44,10 @@ /** * The {@link QbusCommunication} class is able to do the following tasks with Qbus - * systems: + * CTD controllers: *

    @@ -72,7 +74,7 @@ public QbusCommunication(Thing thing) { private @Nullable BufferedReader qIn; private boolean listenerStopped; - private boolean qEventsRunning; + private boolean qbusListenerRunning; private Gson gsonOut = new Gson(); private Gson gsonIn; @@ -80,6 +82,7 @@ public QbusCommunication(Thing thing) { private @Nullable String ctd; private boolean ctdConnected; + private List> outputs = new ArrayList<>(); private final Map bistabiel = new HashMap<>(); private final Map scene = new HashMap<>(); private final Map dimmer = new HashMap<>(); @@ -92,11 +95,22 @@ public QbusCommunication(Thing thing) { private @Nullable QbusBridgeHandler bridgeCallBack; + /** + * Starts main communication thread. + *
      + *
    • Connect to Qbus server + *
    • Requests outputs + *
    • Start listener + *
    + * + * @throws IOException + * @throws InterruptedException + */ public synchronized void startCommunication() throws IOException, InterruptedException { QbusBridgeHandler handler = bridgeCallBack; ctdConnected = false; - if (qEventsRunning) { + if (qbusListenerRunning) { throw new IOException("Previous listening thread is still active."); } @@ -112,8 +126,6 @@ public synchronized void startCommunication() throws IOException, InterruptedExc qSocket = socket; qOut = new PrintWriter(socket.getOutputStream(), true); qIn = new BufferedReader(new InputStreamReader(socket.getInputStream())); - logger.debug("Connected via local port {} from thread {}", socket.getLocalPort(), - Thread.currentThread().getId()); } else { return; } @@ -124,19 +136,21 @@ public synchronized void startCommunication() throws IOException, InterruptedExc // Connect to Qbus server connect(); - // If Qbus Client is connected then initialize and start listening for incoming commands, else put Bridge - // offline - if (ctdConnected) { - initialize(); - threadExecutor.execute(() -> { - try { - qEvents(); - } catch (IOException e) { - logger.debug("Could not start thread {} ", e.getMessage()); - } - }); - } else { - handler.bridgeOffline("No communication with Qbus client"); + // Then start thread to listen to incoming updates from Qbus. + threadExecutor.execute(() -> { + try { + qbusListener(); + } catch (IOException e) { + String msg = e.getMessage(); + logger.warn("Could not start listening thread, IOException: {}", msg); + } catch (InterruptedException e) { + String msg = e.getMessage(); + logger.warn("Could not start listening thread, InterruptedException: {}", msg); + } + }); + + if (!ctdConnected) { + handler.bridgePending("Waiting for CTD to come online..."); } } @@ -159,24 +173,21 @@ public synchronized void stopCommunication() throws IOException { } } - qSocket = null; - - PrintWriter qbusOut = this.qOut; - if (qbusOut != null) { - qbusOut.close(); + BufferedReader reader = this.qIn; + if (reader != null) { + reader.close(); } - BufferedReader qbusIn = this.qIn; - try { - if (qbusIn != null) { - qbusIn.close(); - } - } catch (IOException e) { - logger.debug("Error on closing reader {} ", e.getMessage()); + PrintWriter writer = this.qOut; + if (writer != null) { + writer.close(); } + qSocket = null; + qbusListenerRunning = false; ctdConnected = false; - logger.debug("Communication stopped from thread {}", Thread.currentThread().getId()); + + logger.trace("Communication stopped from thread {}", Thread.currentThread().getId()); } /** @@ -185,41 +196,14 @@ public synchronized void stopCommunication() throws IOException { * @throws InterruptedException * @throws IOException */ - public synchronized void restartCommunication() throws InterruptedException, IOException { stopCommunication(); - logger.debug("Qbus: restart communication from thread {}", Thread.currentThread().getId()); - - try { - startCommunication(); - } catch (InterruptedException | IOException e) { - String message = e.toString(); - logger.debug("Could not start the communication with the server. {}", message); - } + startCommunication(); } /** - * Method to check if communication with Qbus Server is active - * - * @return True if active - */ - public boolean communicationActive() { - return qSocket != null; - } - - /** - * Method to check if communication with Qbus Client is active - * - * @return True if active - */ - - public boolean clientConnected() { - return ctdConnected; - } - - /** - * Runnable that handles incomming communication from Qbus server. + * Thread that handles incoming messages from Qbus client. *

    * The thread listens to the TCP socket opened at instantiation of the {@link QbusCommunication} class * and interprets all incomming json messages. It triggers state updates for active channels linked to the @@ -227,13 +211,15 @@ public boolean clientConnected() { * * @return * @throws IOException + * @throws InterruptedException + * * */ - private void qEvents() throws IOException { + private void qbusListener() throws IOException, InterruptedException { String qMessage; listenerStopped = false; - qEventsRunning = true; + qbusListenerRunning = true; BufferedReader reader = this.qIn; @@ -241,22 +227,34 @@ private void qEvents() throws IOException { throw new IOException("Bufferreader for incoming messages not initialized."); } - while (!Thread.currentThread().isInterrupted() && ((qMessage = reader.readLine()) != null)) { - readMessage(qMessage); + try { + while (!Thread.currentThread().isInterrupted() && ((qMessage = reader.readLine()) != null)) { + readMessage(qMessage); + + } + } catch (IOException e) { + if (!listenerStopped) { + qbusListenerRunning = false; + // the IO has stopped working, so we need to close cleanly and try to restart + restartCommunication(); + return; + } + } finally { + qbusListenerRunning = false; } if (!listenerStopped) { - qEventsRunning = false; + qbusListenerRunning = false; QbusBridgeHandler handler = bridgeCallBack; if (handler != null) { ctdConnected = false; - handler.bridgeOffline("No communication with Qbus server"); + handler.bridgeOffline(ThingStatusDetail.COMMUNICATION_ERROR, "No communication with Qbus server"); } } - qEventsRunning = false; + qbusListenerRunning = false; logger.trace("Event listener thread stopped on thread {}", Thread.currentThread().getId()); }; @@ -265,27 +263,23 @@ private void qEvents() throws IOException { * * @param qMessage * @throws InterruptedException + * @throws IOException */ - synchronized void sendMessage(Object qMessage) throws InterruptedException { + synchronized void sendMessage(Object qMessage) throws InterruptedException, IOException { PrintWriter writer = qOut; String json = gsonOut.toJson(qMessage); - QbusBridgeHandler handler = bridgeCallBack; if (writer != null) { writer.println(json); // Delay after sending data to improve scene execution TimeUnit.MILLISECONDS.sleep(250); - } + if ((writer == null) || (writer.checkError())) { logger.warn("Error sending message, trying to restart communication"); - try { - restartCommunication(); - } catch (IOException e) { - if (handler != null) { - handler.bridgeOffline("No communication with Qbus server. " + e.toString()); - } - } + + restartCommunication(); + // retry sending after restart writer = qOut; if (writer != null) { @@ -298,44 +292,51 @@ synchronized void sendMessage(Object qMessage) throws InterruptedException { } } - /** - * Called by other methods to Qbus server and read response - */ - private void sendAndReadMessage(String command) throws IOException, InterruptedException { - String snr = getSN(); - if (snr != null) { - QbusMessageCmd qCmd = new QbusMessageCmd(snr, command); - - sendMessage(qCmd); - - BufferedReader reader = qIn; - if (reader == null) { - throw new IOException("Cannot read from socket, reader not connected."); - } - readMessage(reader.readLine()); - } else { - QbusBridgeHandler handler = bridgeCallBack; - if (handler != null) { - handler.bridgeOffline("No serial nr defined"); - } - } - } - /** * Method that interprets all feedback from Qbus Server application and calls appropriate handling methods. + *

      + *
    • Get request & update states for Bistabiel/Timers/Intervals/Mono outputs + *
    • Get request & update states for the Scenes + *
    • Get request & update states for Dimmers 1T and 2T + *
    • Get request & update states for Shutters + *
    • Get request & update states for Thermostats + *
    • Get request & update states for CO2 + *
    * * @param qMessage message read from Qbus. + * @throws InterruptedException + * @throws IOException + * */ private void readMessage(String qMessage) { - String cmd = ""; - String ctd = ""; String sn = null; - QbusMessageBase qMessageGson; + String cmd = ""; + String ctd = null; + Integer id = null; + Integer state = null; + Integer mode = null; + Double setpoint = null; + Double measured = null; + Integer slats = null; - qMessageGson = gsonIn.fromJson(qMessage, QbusMessageBase.class); - if (qMessageGson != null) { - ctd = qMessageGson.getSn(); - cmd = qMessageGson.getCmd(); + QbusMessageBase qMessageGson; + try { + qMessageGson = gsonIn.fromJson(qMessage, QbusMessageBase.class); + + if (qMessageGson != null) { + ctd = qMessageGson.getSn(); + cmd = qMessageGson.getCmd(); + id = qMessageGson.getId(); + state = qMessageGson.getState(); + mode = qMessageGson.getMode(); + setpoint = qMessageGson.getSetPoint(); + measured = qMessageGson.getMeasured(); + slats = qMessageGson.getSlatState(); + } + } catch (JsonParseException e) { + String msg = e.getMessage(); + logger.trace("Not acted on unsupported json {} : {}", qMessage, msg); + return; } QbusBridgeHandler handler = bridgeCallBack; @@ -346,126 +347,182 @@ private void readMessage(String qMessage) { if (sn != null && ctd != null) { try { - if (Integer.parseInt(sn) == Integer.parseInt(ctd) && qMessageGson != null) { - // Get the compatible outputs from the Qbus server - if ("returnBistabiel".equals(cmd)) { - cmdListBistabiel(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if ("returnDimmer".equals(cmd)) { - cmdListDimmers(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if (("returnThermostat").equals(cmd)) { - cmdListThermostat(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if (("returnScene").equals(cmd)) { - cmdListScenes(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if (("returnCo2").equals(cmd)) { - cmdListCo2(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if (("returnRol02p").equals(cmd)) { - cmdListRol(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if (("returnSlat").equals(cmd)) { - cmdListRolSlats(((QbusMessageListMap) qMessageGson).getOutputs()); - } - - // Incoming commands from Qbus Client to openHAB (event) - else if ("updateBistabiel".equals(cmd)) { - updateBistabiel(((QbusMessageListMap) qMessageGson).getOutputs()); + if (sn.equals(ctd) && qMessageGson != null) { // Check if commands are for this Bridge + // Handle all outputs from Qbus + if ("returnOutputs".equals(cmd)) { + outputs = ((QbusMessageListMap) qMessageGson).getOutputs(); + + for (Map ctdOutputs : outputs) { + + String ctdType = ctdOutputs.get("type"); + String ctdIdStr = ctdOutputs.get("id"); + Integer ctdId = null; + + if (ctdIdStr != null) { + ctdId = Integer.parseInt(ctdIdStr); + } else { + return; + } + + if (ctdType != null) { + String ctdState = ctdOutputs.get("state"); + String ctdMmode = ctdOutputs.get("regime"); + String ctdSetpoint = ctdOutputs.get("setpoint"); + String ctdMeasured = ctdOutputs.get("measured"); + String ctdSlats = ctdOutputs.get("slats"); + + Integer ctdStateI = null; + if (ctdState != null) { + ctdStateI = Integer.parseInt(ctdState); + } + + Integer ctdSlatsI = null; + if (ctdSlats != null) { + ctdSlatsI = Integer.parseInt(ctdSlats); + } + + Integer ctdMmodeI = null; + if (ctdMmode != null) { + ctdMmodeI = Integer.parseInt(ctdMmode); + } + + Double ctdSetpointD = null; + if (ctdSetpoint != null) { + ctdSetpointD = Double.parseDouble(ctdSetpoint); + } + + Double ctdMeasuredD = null; + if (ctdMeasured != null) { + ctdMeasuredD = Double.parseDouble(ctdMeasured); + } + + if (ctdState != null) { + if (ctdType.equals("bistabiel")) { + QbusBistabiel output = new QbusBistabiel(ctdId); + if (!bistabiel.containsKey(ctdId)) { + output.setQComm(this); + output.updateState(ctdStateI); + bistabiel.put(ctdId, output); + } else { + output.updateState(ctdStateI); + } + } else if (ctdType.equals("dimmer")) { + QbusDimmer output = new QbusDimmer(ctdId); + if (!dimmer.containsKey(ctdId)) { + output.setQComm(this); + output.updateState(ctdStateI); + dimmer.put(ctdId, output); + } else { + output.updateState(ctdStateI); + } + } else if (ctdType.equals("CO2")) { + QbusCO2 output = new QbusCO2(); + if (!co2.containsKey(ctdId)) { + output.updateState(ctdStateI); + co2.put(ctdId, output); + } else { + output.updateState(ctdStateI); + } + } else if (ctdType.equals("scene")) { + QbusScene output = new QbusScene(ctdId); + if (!scene.containsKey(ctdId)) { + output.setQComm(this); + scene.put(ctdId, output); + } + } else if (ctdType.equals("rol")) { + QbusRol output = new QbusRol(ctdId); + if (!rol.containsKey(ctdId)) { + output.setQComm(this); + output.updateState(ctdStateI); + if (ctdSlats != null) { + output.updateSlats(ctdSlatsI); + } + rol.put(ctdId, output); + } else { + output.updateState(ctdStateI); + if (ctdSlats != null) { + output.updateSlats(ctdSlatsI); + } + } + } + } else if (ctdMeasuredD != null && ctdSetpointD != null && ctdMmodeI != null) { + if (ctdType.equals("thermostat")) { + QbusThermostat output = new QbusThermostat(ctdId); + if (!thermostat.containsKey(ctdId)) { + output.setQComm(this); + output.updateState(ctdMeasuredD, ctdSetpointD, ctdMmodeI); + thermostat.put(ctdId, output); + } else { + output.updateState(ctdMeasuredD, ctdSetpointD, ctdMmodeI); + } + } + } + } + } + // Handle update commands from Qbus + } else if ("updateBistabiel".equals(cmd)) { + if (id != null && state != null) { + updateBistabiel(id, state); + } } else if ("updateDimmer".equals(cmd)) { - updateDimmer(((QbusMessageListMap) qMessageGson).getOutputs()); - } else if ("updateThermostat".equals(cmd)) { - updateThermostat(((QbusMessageListMap) qMessageGson).getOutputs()); + if (id != null && state != null) { + updateDimmer(id, state); + } + } else if ("updateDimmer".equals(cmd)) { + if (id != null && state != null) { + updateDimmer(id, state); + } } else if ("updateCo2".equals(cmd)) { - updateCO2(((QbusMessageListMap) qMessageGson).getOutputs()); + if (id != null && state != null) { + updateCO2(id, state); + } + } else if ("updateThermostat".equals(cmd)) { + if (id != null && measured != null && setpoint != null && mode != null) { + updateThermostat(id, mode, setpoint, measured); + } } else if ("updateRol02p".equals(cmd)) { - updateRol(((QbusMessageListMap) qMessageGson).getOutputs()); + if (id != null && state != null) { + updateRol(id, state); + } } else if ("updateRol02pSlat".equals(cmd)) { - updateRolSlats(((QbusMessageListMap) qMessageGson).getOutputs()); - } - - // Incomming commands from Qbus server to verify the client connection - else if ("disconnect".equals(cmd)) { + if (id != null && state != null && slats != null) { + updateRolSlats(id, state, slats); + } + // Incomming commands from Qbus server to verify the client connection + } else if ("noconnection".equals(cmd)) { eventDisconnect(); - } else if ("notConnected".equals(cmd)) { - noConnection(); } else if ("connected".equals(cmd)) { - connection(); + // threadExecutor.execute(() -> { + try { + requestOutputs(); + } catch (InterruptedException e) { + String msg = e.getMessage(); + logger.warn("Could not request outputs. InterruptedException: {}", msg); + } catch (IOException e) { + String msg = e.getMessage(); + logger.warn("Could not request outputs. IOException: {}", msg); + } } } - } catch (JsonParseException e) { - logger.warn("Not acted on unsupported json {}", qMessage); + String msg = e.getMessage(); + logger.warn("Not acted on unsupported json {}, {}", qMessage, msg); } } } /** - * After setting up the communication with the Qbus Server, send all initialization messages. - *

    - * First send connect to connect with the Qbus Server application - * Get request for Bistabiel/Timers/Intervals/Mono outputs - * Get request for the Scenes - * Get request for Dimmers 1T and 2T - * Get request for Shutters - * Get request for Thermostats - * Get request for CO2 + * Initialize the communication object */ @Override public void initialize() { - QbusBridgeHandler handler = bridgeCallBack; - - if (bridgeCallBack != null) { - if (ctdConnected) { - try { - sendAndReadMessage("getBistabiel"); - sendAndReadMessage("getScene"); - sendAndReadMessage("getDimmer"); - sendAndReadMessage("getRol02p"); - sendAndReadMessage("getRol02pSlat"); - sendAndReadMessage("getThermostat"); - sendAndReadMessage("getCo2"); - } catch (IOException e) { - String message = e.toString(); - if (handler != null) { - handler.bridgeOffline("Could not request outputs from client. {}" + message); - } else { - logger.debug("Could not request outputs from client. {}", message); - } - } catch (InterruptedException e) { - String message = e.toString(); - if (handler != null) { - handler.bridgeOffline("Could not request outputs from client. {}" + message); - } else { - logger.debug("Could not request outputs from client. {}", message); - } - } - - } else { - ctdConnected = false; - - if (handler != null) { - handler.bridgeOffline("No communication with Qbus client"); - } - - return; - } - } else { - logger.trace("Initialization error"); - } - } - - public @Nullable String getSN() { - return this.ctd; - } - - public void setSN() { - QbusBridgeHandler qBridgeHandler = bridgeCallBack; - if (qBridgeHandler != null) { - this.ctd = qBridgeHandler.getSn(); - } } /** * Initial connection to Qbus Server to open a communication channel * + * @throws InterruptedException * @throws IOException - * */ private void connect() throws InterruptedException, IOException { String snr = getSN(); @@ -474,414 +531,172 @@ private void connect() throws InterruptedException, IOException { QbusMessageCmd qCmd = new QbusMessageCmd(snr, "openHAB"); sendMessage(qCmd); + BufferedReader reader = qIn; if (reader == null) { throw new IOException("Cannot read from socket, reader not connected."); } readMessage(reader.readLine()); + } else { QbusBridgeHandler handler = bridgeCallBack; if (handler != null) { - handler.bridgeOffline("No serial nr defined"); + handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "No serial nr defined"); } } - - return; } /** - * Get all the Bistabiel/Timer/Mono/Interval outputs from the Qbus client + * Send a request for all available outputs and initializes them via readMessage * - * @param data + * @throws InterruptedException + * @throws IOException */ - private void cmdListBistabiel(@Nullable List> outputs) { - if (outputs != null) { - for (Map bistabiel : outputs) { - String idStr = bistabiel.get("id"); - String stateStr = bistabiel.get("state"); - if (idStr != null && stateStr != null) { - int id = Integer.parseInt(idStr); - Integer state = Integer.parseInt(stateStr); - QbusBistabiel qBistabiel = new QbusBistabiel(idStr); - if (!this.bistabiel.containsKey(id)) { - qBistabiel.setState(state); - qBistabiel.setQComm(this); - this.bistabiel.put(id, qBistabiel); - qBistabiel.setState(state); - } else { - qBistabiel.setState(state); - } - } else { - logger.debug("Error in json for BistabBistabiel/Timers/Monos/Intervals"); - } - } - } - } + private void requestOutputs() throws InterruptedException, IOException { + String snr = getSN(); + QbusBridgeHandler handler = bridgeCallBack; - /** - * Get all the scenes from the Qbus server - * - * @param outputs - */ - private void cmdListScenes(@Nullable List> outputs) { - if (outputs != null) { - for (Map scene : outputs) { - String idStr = scene.get("id"); - if (idStr != null) { - int id = Integer.parseInt(idStr); - QbusScene qScene = new QbusScene(idStr); - qScene.setQComm(this); - this.scene.put(id, qScene); - } else { - logger.debug("Error in json for Scenes"); - } - } - } - } + if (snr != null) { + QbusMessageCmd qCmd = new QbusMessageCmd(snr, "all"); + sendMessage(qCmd); - /** - * Get all the Dimmer outputs from the Qbus client - * - * @param outputs - */ - private void cmdListDimmers(@Nullable List> outputs) { - if (outputs != null) { - for (Map dimmer : outputs) { - String idStr = dimmer.get("id"); - String stateStr = dimmer.get("state"); - if (idStr != null && stateStr != null) { - int id = Integer.parseInt(idStr); - Integer state = Integer.parseInt(stateStr); - QbusDimmer qDimmer = new QbusDimmer(idStr); - if (!this.dimmer.containsKey(id)) { - qDimmer.setQComm(this); - this.dimmer.put(id, qDimmer); - qDimmer.updateState(state); - } else { - qDimmer.updateState(state); - } - } else { - logger.debug("Error in json for Dimmer"); - } + BufferedReader reader = qIn; + if (reader == null) { + throw new IOException("Cannot read from socket, reader not connected."); } - } - } + readMessage(reader.readLine()); + ctdConnected = true; - /** - * Get all the screens with slat control outputs from the Qbus client - * - * @param data - */ - private void cmdListRol(@Nullable List> outputs) { - if (outputs != null) { - for (Map rol : outputs) { - String idStr = rol.get("id"); - String stateStr = rol.get("state"); - if (idStr != null && stateStr != null) { - int id = Integer.parseInt(idStr); - Integer rolpos = Integer.valueOf(stateStr); - QbusRol qRol = new QbusRol(idStr); - if (!this.rol.containsKey(id)) { - qRol.setQComm(this); - this.rol.put(id, qRol); - qRol.setState(rolpos); - } else { - qRol.setState(rolpos); - } - } else { - logger.debug("Error in json for ROL02P"); - } + if (handler != null) { + handler.bridgeOnline(); } - } - } - /** - * Get all the screen outputs from the Qbus client - * - * @param data - */ - private void cmdListRolSlats(@Nullable List> outputs) { - if (outputs != null) { - for (Map rol : outputs) { - String idStr = rol.get("id"); - String rolPos = rol.get("rolPos"); - String slatPos = rol.get("slatPos"); - if (idStr != null && rolPos != null && slatPos != null) { - int id = Integer.parseInt(idStr); - Integer rolpos = Integer.parseInt(rolPos); - Integer rolposslats = Integer.parseInt(slatPos); - QbusRol qRol = new QbusRol(idStr); - if (!this.rol.containsKey(id)) { - qRol.setQComm(this); - this.rol.put(id, qRol); - qRol.setState(rolpos); - qRol.setSlats(rolposslats); - } else { - qRol.setState(rolpos); - qRol.setSlats(rolposslats); - } - } else { - logger.debug("Error in json for ROL02P_Slats"); - } + } else { + if (handler != null) { + handler.bridgeOffline(ThingStatusDetail.CONFIGURATION_ERROR, "No serial nr defined"); } } } /** - * Get all the CO2 outputs from the Qbus client + * Event on incoming Bistabiel/Timer/Mono/Interval updates * - * @param data + * @param id + * @param state */ - private void cmdListCo2(@Nullable List> outputs) { - if (outputs != null) { - for (Map co2 : outputs) { - String idStr = co2.get("id"); - String stateStr = co2.get("state"); - if (idStr != null && stateStr != null) { - int id = Integer.parseInt(idStr); - int state = Integer.parseInt(stateStr); - QbusCO2 qCO2 = new QbusCO2(); - if (!this.co2.containsKey(id)) { - this.co2.put(id, qCO2); - qCO2.setState(state); - } else { - qCO2.setState(state); - } - } else { - logger.debug("Error in json for CO2"); - } + private void updateBistabiel(Integer id, Integer state) { + QbusBistabiel qBistabiel = this.bistabiel.get(id); - } + if (qBistabiel != null) { + qBistabiel.updateState(state); + } else { + logger.trace("Bistabiel in controller not known {}", id); } } /** - * Get all the Thermostat outputs from the Qbus client + * Event on incoming Dimmer updates * - * @param data + * @param id + * @param state */ - private void cmdListThermostat(@Nullable List> outputs) { - if (outputs != null) { - for (Map thermostat : outputs) { - String idStr = thermostat.get("id"); - String measuredStr = thermostat.get("measured"); - String setpointStr = thermostat.get("SetPoint"); - String modeStr = thermostat.get("Mode"); - if (idStr != null && measuredStr != null && setpointStr != null && modeStr != null) { - int id = Integer.parseInt(idStr); - Double measured = Double.valueOf(measuredStr); - Double setpoint = Double.valueOf(setpointStr); - Integer mode = Integer.valueOf(modeStr); - QbusThermostat qThermostat = new QbusThermostat(idStr); - if (!this.thermostat.containsKey(id)) { - qThermostat.updateState(measured, setpoint, mode); - qThermostat.setQComm(this); - this.thermostat.put(id, qThermostat); - } else { - qThermostat.updateState(measured, setpoint, mode); - } - } else { - logger.debug("Error in json for Thermostats"); - } - } + private void updateDimmer(Integer id, Integer state) { + QbusDimmer qDimmer = this.dimmer.get(id); + + if (qDimmer != null) { + qDimmer.updateState(state); + } else { + logger.trace("Dimmer in controller not known {}", id); } } /** - * Event on incoming Bistabiel/Timer/Mono/Interval updates + * Event on incoming thermostat updates * - * @param data + * @param id + * @param mode + * @param sp + * @param ct */ - private void updateBistabiel(List> output) { - for (Map bistabiel : output) { - String idStr = bistabiel.get("id"); - String stateStr = bistabiel.get("state"); - if (idStr != null && stateStr != null) { - int id = Integer.parseInt(idStr); - int state = Integer.parseInt(stateStr); - QbusBistabiel qBistabiel = this.bistabiel.get(id); - if (qBistabiel != null) { - if (!this.bistabiel.containsKey(id)) { - logger.warn("Bistabiel in controller not known {}", id); - return; - } - logger.debug("Event execute bistabiel {} with state {}", id, state); - qBistabiel.setState(state); - } - } + private void updateThermostat(Integer id, int mode, double sp, double ct) { + QbusThermostat qThermostat = this.thermostat.get(id); + + if (qThermostat != null) { + qThermostat.updateState(ct, sp, mode); + } else { + logger.trace("Thermostat in controller not known {}", id); } } /** - * Event on incoming Dimmer updates + * Event on incoming CO2 updates * - * @param data + * @param id + * @param state */ - private void updateDimmer(List> data) { - for (Map dimmer : data) { - String idStr = dimmer.get("id"); - String stateStr = dimmer.get("state"); - if (idStr != null && stateStr != null) { - int id = Integer.valueOf(idStr); - int value = Integer.valueOf(stateStr); - QbusDimmer qDimmer = this.dimmer.get(id); - if (!this.dimmer.containsKey(id)) { - logger.warn("Dimmer in controller not known {}", id); - return; - } - if (qDimmer != null) { - logger.debug("Event execute dimmer {} with state {}", id, value); - qDimmer.setState(value); - } - } + private void updateCO2(Integer id, Integer state) { + QbusCO2 qCO2 = this.co2.get(id); + + if (qCO2 != null) { + qCO2.updateState(state); + } else { + logger.trace("CO2 in controller not known {}", id); } } /** * Event on incoming screen updates * - * @param data + * @param id + * @param state */ - private void updateRol(List> data) { - for (Map rol : data) { - String idStr = rol.get("id"); - String posStr = rol.get("value1Str"); - if (idStr != null && posStr != null) { - int id = Integer.valueOf(idStr); - int pos = Integer.valueOf(posStr); - QbusRol qRol = this.rol.get(id); - if (!this.rol.containsKey(id)) { - logger.warn("Rol02p in controller not known {}", id); - return; - } - if (qRol != null) { - logger.debug("Event execute Rol02P {} with pos {}", id, pos); - qRol.setState(pos); - } + private void updateRol(Integer id, Integer state) { + QbusRol qRol = this.rol.get(id); - } + if (qRol != null) { + qRol.updateState(state); + } else { + logger.trace("ROL02P in controller not known {}", id); } } /** * Event on incoming screen with slats updates * - * @param data + * @param id + * @param state + * @param slats */ - private void updateRolSlats(List> data) { - for (Map rol : data) { - String idStr = rol.get("id"); - String posStr = rol.get("pos"); - String slatsStr = rol.get("slats"); - if (idStr != null && posStr != null && slatsStr != null) { - int id = Integer.valueOf(idStr); - int pos = Integer.valueOf(posStr); - int slats = Integer.valueOf(slatsStr); - QbusRol qRol = this.rol.get(id); - if (!this.rol.containsKey(id)) { - logger.warn("Rol02p in controller not known {}", id); - return; - } - if (qRol != null) { - logger.debug("Event execute ROL02P_Slats {} with pos {} and slats {}", id, pos, slats); - qRol.setState(pos); - qRol.setSlats(slats); - } - } - } - } + private void updateRolSlats(Integer id, Integer state, Integer slats) { + QbusRol qRol = this.rol.get(id); - /** - * Event on incoming thermostat updates - * - * @param data - */ - private void updateThermostat(List> data) { - for (Map thermostat : data) { - String idStr = thermostat.get("id"); - String measuredStr = thermostat.get("measured"); - String setpointdStr = thermostat.get("setpoint"); - String modedStr = thermostat.get("mode"); - if (idStr != null && measuredStr != null && setpointdStr != null && modedStr != null) { - int id = Integer.valueOf(idStr); - Double measured = Double.valueOf(measuredStr); - Double setpoint = Double.valueOf(setpointdStr); - Integer mode = Integer.valueOf(modedStr); - QbusThermostat qThermostat = this.thermostat.get(id); - if (!this.thermostat.containsKey(id)) { - logger.warn("Thermostat in controller not known {}", id); - return; - } - if (qThermostat != null) { - logger.debug("Event execute thermostat {} with measured {}, setpoint {}, mode {}", id, measured, - setpoint, mode); - qThermostat.updateState(measured, setpoint, mode); - } - } + if (qRol != null) { + qRol.updateState(state); + qRol.updateSlats(slats); + } else { + logger.trace("ROL02P with slats in controller not known {}", id); } } /** - * Event on incoming CO2 updates + * Put Bridge offline when there is no connection from the QbusClient * - * @param data - */ - private void updateCO2(List> data) { - for (Map co2 : data) { - String idStr = co2.get("id"); - String value1Str = co2.get("value1Str"); - if (idStr != null && value1Str != null) { - int id = Integer.valueOf(idStr); - int value1 = Integer.valueOf(value1Str); - QbusCO2 qCO2 = this.co2.get(id); - if (!this.co2.containsKey(id)) { - logger.warn("Co2 in controller not known {}", id); - return; - } - if (qCO2 != null) { - logger.debug("Event execute co2 {} with state {}", id, value1); - qCO2.setState(value1); - } - } - } - } - - /** - * Put Bridge offline when QbusClient disconnects */ private void eventDisconnect() { - ctdConnected = false; QbusBridgeHandler handler = bridgeCallBack; - if (handler != null) { - handler.bridgeOffline("No communication with Qbus client"); - } - } - /** - * Put Bridge offline when there is no connection from the QbusClient - */ - private void noConnection() { - ctdConnected = false; - QbusBridgeHandler handler = bridgeCallBack; if (handler != null) { - handler.bridgeOffline("No communication with Qbus client"); + handler.bridgePending("Waiting for CTD connection"); } } - /** - * Set connection state true if there is a connection from QbusClient - */ - private void connection() { - ctdConnected = true; - } - /** * Return all Bistabiel/Timers/Mono/Intervals in the Qbus Controller. * * @return */ - public @Nullable Map getBistabiel() { + public Map getBistabiel() { return this.bistabiel; } @@ -890,46 +705,64 @@ private void connection() { * * @return */ - public @Nullable Map getScene() { + public Map getScene() { return this.scene; } /** - * Return all Dimmers in the Qbus Controller. + * Return all Dimmers outputs in the Qbus Controller. * * @return */ - public @Nullable Map getDimmer() { + public Map getDimmer() { return this.dimmer; } /** - * Return all rollershutter outûts in the Qbus Controller. + * Return all rollershutter/screen outputs in the Qbus Controller. * * @return */ - public @Nullable Map getRol() { + public Map getRol() { return this.rol; } /** - * Return all Thermostats in the Qbus Controller. + * Return all Thermostats outputs in the Qbus Controller. * * @return */ - public @Nullable Map getThermostat() { + public Map getThermostat() { return this.thermostat; } /** - * Return all CO2 in the Qbus Controller. + * Return all CO2 outputs in the Qbus Controller. * * @return */ - public @Nullable Map getCo2() { + public Map getCo2() { return this.co2; } + /** + * Method to check if communication with Qbus Server is active + * + * @return True if active + */ + public boolean communicationActive() { + return qSocket != null; + } + + /** + * Method to check if communication with Qbus Client is active + * + * @return True if active + */ + public boolean clientConnected() { + return ctdConnected; + } + /** * @param bridgeCallBack the bridgeCallBack to set */ @@ -937,6 +770,26 @@ public void setBridgeCallBack(QbusBridgeHandler bridgeCallBack) { this.bridgeCallBack = bridgeCallBack; } + /** + * Get the serial number of the CTD as configured in the Bridge. + * + * @return serial number of controller + */ + public @Nullable String getSN() { + return this.ctd; + } + + /** + * Sets the serial number of the CTD, as configured in the Bridge. + */ + public void setSN() { + QbusBridgeHandler qBridgeHandler = bridgeCallBack; + + if (qBridgeHandler != null) { + this.ctd = qBridgeHandler.getSn(); + } + } + @Override public void handleCommand(ChannelUID channelUID, Command command) { } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java index a879ec7323856..a7d61973775a4 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusDimmer.java @@ -12,11 +12,11 @@ */ package org.openhab.binding.qbus.internal.protocol; +import java.io.IOException; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusDimmerHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusDimmer} class represents the action Qbus Dimmer output. @@ -27,38 +27,22 @@ @NonNullByDefault public final class QbusDimmer { - private final Logger logger = LoggerFactory.getLogger(QbusDimmer.class); - private @Nullable QbusCommunication qComm; - private String id; + private Integer id; private @Nullable Integer state; private @Nullable QbusDimmerHandler thingHandler; - QbusDimmer(String id) { + QbusDimmer(Integer id) { this.id = id; } - /** - * Update all values of the dimmer - * - * @param state - */ - public void updateState(Integer state) { - setState(state); - - QbusDimmerHandler handler = thingHandler; - if (handler != null) { - handler.handleStateUpdate(this); - } - } - /** * This method should be called if the ThingHandler for the thing corresponding to this dimmer is initialized. * It keeps a record of the thing handler in this object so the thing can be updated when - * the dimmer receives an update from the Qbus IP-interface. + * the dimmer receives an update from the Qbus client. * * @param handler */ @@ -69,7 +53,7 @@ public void setThingHandler(QbusDimmerHandler handler) { /** * This method sets a pointer to the qComm Dimmer of class {@link QbusCommuncation}. * This is then used to be able to call back the sendCommand method in this class to send a command to the - * Qbus IP-interface when.. + * Qbus client. * * @param qComm */ @@ -78,20 +62,29 @@ public void setQComm(QbusCommunication qComm) { } /** - * Get state of dimmer. + * Update the value of the dimmer + * + * @param state + */ + public void updateState(@Nullable Integer state) { + this.state = state; + QbusDimmerHandler handler = this.thingHandler; + if (handler != null) { + handler.handleStateUpdate(this); + } + } + + /** + * Get the state of dimmer. * * @return dimmer state */ public @Nullable Integer getState() { - if (this.state != null) { - return this.state; - } else { - return null; - } + return this.state; } /** - * Sets state of Dimmer. + * Sets the state of Dimmer. * * @param dimmer state */ @@ -104,17 +97,16 @@ void setState(int state) { } /** - * Sends Dimmer state to Qbus. + * Sends the dimmer state to Qbus. + * + * @throws IOException + * @throws InterruptedException */ - public void execute(int percent, String sn) { + public void execute(int percent, String sn) throws InterruptedException, IOException { QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeDimmer").withId(this.id).withState(percent); - QbusCommunication comm = qComm; + QbusCommunication comm = this.qComm; if (comm != null) { - try { - comm.sendMessage(qCmd); - } catch (InterruptedException e) { - logger.warn("Could not send command for dimmer {}, {}", this.id, e.getMessage()); - } + comm.sendMessage(qCmd); } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java index 6c1f2365a8683..5c75d1cb7b196 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageBase.java @@ -30,10 +30,12 @@ abstract class QbusMessageBase { private @Nullable String ctd; protected @Nullable String cmd; - protected @Nullable String id; + protected @Nullable String type; + protected @Nullable Integer id; protected @Nullable Integer state; protected @Nullable Integer mode; protected @Nullable Double setpoint; + protected @Nullable Double measured; protected @Nullable Integer slatState; @Nullable @@ -41,8 +43,8 @@ String getSn() { return this.ctd; } - void setSn(String cTD) { - this.ctd = cTD; + void setSn(String ctd) { + this.ctd = ctd; } @Nullable @@ -55,11 +57,20 @@ void setCmd(String cmd) { } @Nullable - public String getId() { + public Integer getId() { return id; } - public void setId(String id) { + public void setType(String type) { + this.type = type; + } + + @Nullable + public String getType() { + return type; + } + + public void setId(Integer id) { this.id = id; } @@ -90,6 +101,15 @@ public void setSetPoint(Double setpoint) { this.setpoint = setpoint; } + @Nullable + public Double getMeasured() { + return measured; + } + + public void setMeasured(Double measured) { + this.measured = measured; + } + @Nullable public Integer getSlatState() { return slatState; diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java index 9ee5eb63c657c..f7e7d031e1684 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageCmd.java @@ -35,7 +35,7 @@ class QbusMessageCmd extends QbusMessageBase { this.cmd = cmd; } - QbusMessageCmd withId(String id) { + QbusMessageCmd withId(Integer id) { this.setId(id); return this; } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java index 746618be7c05f..2d06f72cb39c0 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusMessageDeserializer.java @@ -45,66 +45,113 @@ class QbusMessageDeserializer implements JsonDeserializer { final JsonDeserializationContext context) throws JsonParseException { final JsonObject jsonObject = json.getAsJsonObject(); + String ctd = null; + String cmd = null; + Integer id = null; + Integer state = null; + Integer mode = null; + Double measured = null; + Double setpoint = null; + Integer slats = null; + + QbusMessageBase message = null; + + JsonElement jsonOutputs = null; try { - String cmd = null; - String ctd = null; + if (jsonObject.has("CTD")) { + ctd = jsonObject.get("CTD").getAsString(); + } if (jsonObject.has("cmd")) { cmd = jsonObject.get("cmd").getAsString(); } - if (jsonObject.has("CTD")) { - ctd = jsonObject.get("CTD").getAsString(); + if (jsonObject.has("id")) { + id = jsonObject.get("id").getAsInt(); + } + + if (jsonObject.has("state")) { + state = jsonObject.get("state").getAsInt(); } - JsonElement jsonOutputs = null; + if (jsonObject.has("mode")) { + mode = jsonObject.get("mode").getAsInt(); + } + + if (jsonObject.has("measured")) { + measured = jsonObject.get("measured").getAsDouble(); + } + + if (jsonObject.has("setpoint")) { + setpoint = jsonObject.get("setpoint").getAsDouble(); + } + + if (jsonObject.has("slats")) { + slats = jsonObject.get("slats").getAsInt(); + } if (jsonObject.has("outputs")) { jsonOutputs = jsonObject.get("outputs"); + } - QbusMessageBase message = null; + if (ctd != null && cmd != null) { + if (jsonOutputs != null) { + if (jsonOutputs.isJsonArray()) { + JsonArray jsonOutputsArray = jsonOutputs.getAsJsonArray(); + message = new QbusMessageListMap(); + message.setCmd(cmd); + message.setSn(ctd); + + List> outputsList = new ArrayList<>(); + for (int i = 0; i < jsonOutputsArray.size(); i++) { + JsonObject jsonOutputsObject = jsonOutputsArray.get(i).getAsJsonObject(); + + Map outputs = new HashMap<>(); + for (Entry entry : jsonOutputsObject.entrySet()) { + outputs.put(entry.getKey(), entry.getValue().getAsString()); + } + outputsList.add(outputs); + } + ((QbusMessageListMap) message).setOutputs(outputsList); + } - if (jsonOutputs != null) { - if (jsonOutputs.isJsonObject()) { + } else { message = new QbusMessageMap(); - Map outputs = new HashMap<>(); - for (Entry entry : jsonOutputs.getAsJsonObject().entrySet()) { - outputs.put(entry.getKey(), entry.getValue().getAsString()); + message.setCmd(cmd); + message.setSn(ctd); + + if (id != null) { + message.setId(id); } - ((QbusMessageMap) message).setOutputs(outputs); - } else if (jsonOutputs.isJsonArray()) { - JsonArray jsonOutputsArray = jsonOutputs.getAsJsonArray(); + if (state != null) { + message.setState(state); + } - message = new QbusMessageListMap(); + if (slats != null) { + message.setSlatState(slats); + } - List> outputsList = new ArrayList<>(); - for (int i = 0; i < jsonOutputsArray.size(); i++) { - JsonObject jsonOutputsObject = jsonOutputsArray.get(i).getAsJsonObject(); + if (mode != null) { + message.setMode(mode); + } - Map outputs = new HashMap<>(); - for (Entry entry : jsonOutputsObject.entrySet()) { - outputs.put(entry.getKey(), entry.getValue().getAsString()); - } - outputsList.add(outputs); + if (measured != null) { + message.setMeasured(measured); } - ((QbusMessageListMap) message).setOutputs(outputsList); - } - } - if (message != null && cmd != null && ctd != null) { - message.setCmd(cmd); - message.setSn(ctd); - } else { - throw new JsonParseException("Unexpected Json type "); - } + if (setpoint != null) { + message.setSetPoint(setpoint); + } + } + } return message; - } catch (IllegalStateException e) { - throw new JsonParseException("Unexpected Json type {} ", e); + String mess = e.getMessage(); + throw new JsonParseException("Unexpected Json format " + mess + " for " + jsonObject.toString()); } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java index 3e2646a2d0bb1..4358e7e27176b 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusRol.java @@ -12,11 +12,11 @@ */ package org.openhab.binding.qbus.internal.protocol; +import java.io.IOException; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusRolHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusRol} class represents the action Qbus Shutter/Slats output. @@ -27,11 +27,9 @@ @NonNullByDefault public final class QbusRol { - private final Logger logger = LoggerFactory.getLogger(QbusRol.class); - private @Nullable QbusCommunication qComm; - private String id; + private Integer id; private @Nullable Integer state; @@ -39,7 +37,7 @@ public final class QbusRol { private @Nullable QbusRolHandler thingHandler; - QbusRol(String id) { + QbusRol(Integer id) { this.id = id; } @@ -47,7 +45,7 @@ public final class QbusRol { * This method should be called if the ThingHandler for the thing corresponding to this Shutter/Slats is * initialized. * It keeps a record of the thing handler in this object so the thing can be updated when - * the shutter/slat receives an update from the Qbus IP-interface. + * the shutter/slat receives an update from the Qbus client. * * @param qbusRolHandler */ @@ -67,84 +65,74 @@ public void setQComm(QbusCommunication qComm) { } /** - * Get state of shutter. + * Update the value of the Shutter. * - * @return shutter state + * @param Shutter value */ - public @Nullable Integer getState() { - if (this.state != null) { - return this.state; - } else { - return null; + public void updateState(@Nullable Integer state) { + this.state = state; + QbusRolHandler handler = this.thingHandler; + if (handler != null) { + handler.handleStateUpdate(this); } } /** - * Get state of slats. + * Update the value of the Slats. * - * @return slats state + * @param Slat value */ - public @Nullable Integer getStateSlats() { - if (this.slats != null) { - return this.slats; - } else { - return null; + public void updateSlats(@Nullable Integer Slats) { + this.slats = Slats; + QbusRolHandler handler = this.thingHandler; + if (handler != null) { + handler.handleStateUpdate(this); } } /** - * Sets state of Shutter. + * Get the value of the Shutter. * - * @param shutter state + * @return shutter value */ - public void setState(Integer stat) { - this.state = stat; - QbusRolHandler handler = this.thingHandler; - if (handler != null) { - handler.handleStateUpdate(this); - } + public @Nullable Integer getState() { + return this.state; } /** - * Sets state of Slats. + * Get the value of the Slats. * - * @param slats state + * @return slats value */ - public void setSlats(Integer Slats) { - this.slats = Slats; - QbusRolHandler handler = this.thingHandler; - if (handler != null) { - handler.handleStateUpdate(this); - } + public @Nullable Integer getStateSlats() { + return this.slats; } /** - * Sends shutter to Qbus. + * Sends shutter state to Qbus. + * + * @throws IOException + * @throws InterruptedException */ - public void execute(int value, String sn) { + public void execute(int value, String sn) throws InterruptedException, IOException { QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeStore").withId(this.id).withState(value); QbusCommunication comm = qComm; if (comm != null) { - try { - comm.sendMessage(qCmd); - } catch (InterruptedException e) { - logger.warn("Could not send command for store {}, {}", this.id, e.getMessage()); - } + comm.sendMessage(qCmd); } } /** - * Sends slats to Qbus. + * Sends slats state to Qbus. + * + * @throws IOException + * @throws InterruptedException */ - public void executeSlats(int value, String sn) { - QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeSlats").withId(this.id).withSlatState(value); + public void executeSlats(int value, String sn) throws InterruptedException, IOException { + QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeSlats").withId(this.id).withState(value); QbusCommunication comm = qComm; if (comm != null) { - try { - comm.sendMessage(qCmd); - } catch (InterruptedException e) { - logger.warn("Could not send command for slat {}, {}", this.id, e.getMessage()); - } + comm.sendMessage(qCmd); } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java index d693159c6aa59..bc5fac0c40726 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusScene.java @@ -12,11 +12,11 @@ */ package org.openhab.binding.qbus.internal.protocol; +import java.io.IOException; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusSceneHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusScene} class represents the action Qbus Scene output. @@ -27,24 +27,33 @@ @NonNullByDefault public final class QbusScene { - private final Logger logger = LoggerFactory.getLogger(QbusScene.class); - private @Nullable QbusCommunication qComm; public @Nullable QbusSceneHandler thingHandler; private @Nullable Integer state; - private String id; + private Integer id; - QbusScene(String id) { + QbusScene(Integer id) { this.id = id; } /** - * This method sets a pointer to the qComm Scene of class {@link QbusCommuncation}. + * This method should be called if the ThingHandler for the thing corresponding to this scene is initialized. + * It keeps a record of the thing handler in this object so the thing can be updated when + * the scene output receives an update from the Qbus client. + * + * @param handler + */ + public void setThingHandler(QbusSceneHandler handler) { + this.thingHandler = handler; + } + + /** + * This method sets a pointer to the qComm SCENE of class {@link QbusCommuncation}. * This is then used to be able to call back the sendCommand method in this class to send a command to the - * Qbus IP-interface when.. + * Qbus client. * * @param qComm */ @@ -53,34 +62,27 @@ public void setQComm(QbusCommunication qComm) { } /** - * Sends action to Qbus. + * Get the value of the Scene. + * + * @return Scene value */ - public void execute(int value, String sn) { - QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeScene").withId(this.id).withState(value); - QbusCommunication comm = qComm; - if (comm != null) { - try { - comm.sendMessage(qCmd); - } catch (InterruptedException e) { - logger.warn("Could not send command for scene {}, {}", this.id, e.getMessage()); - } - } - } - - public void setThingHandler(QbusSceneHandler handler) { - this.thingHandler = handler; + public @Nullable Integer getState() { + return this.state; } /** - * Get state of bistabiel. - * - * @return bistabiel state + * Sends Scene state to Qbus. + * + * @param value + * @param sn + * @throws InterruptedException + * @throws IOException */ - public @Nullable Integer getState() { - if (this.state != null) { - return this.state; - } else { - return null; + public void execute(int value, String sn) throws InterruptedException, IOException { + QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeScene").withId(this.id).withState(value); + QbusCommunication comm = qComm; + if (comm != null) { + comm.sendMessage(qCmd); } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java index fa9b14654d727..493d6b6b548b5 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusThermostat.java @@ -12,11 +12,11 @@ */ package org.openhab.binding.qbus.internal.protocol; +import java.io.IOException; + import org.eclipse.jdt.annotation.NonNullByDefault; import org.eclipse.jdt.annotation.Nullable; import org.openhab.binding.qbus.internal.handler.QbusThermostatHandler; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; /** * The {@link QbusThermostat} class represents the thermostat Qbus communication object. It contains all @@ -29,43 +29,23 @@ @NonNullByDefault public final class QbusThermostat { - private final Logger logger = LoggerFactory.getLogger(QbusThermostat.class); - private @Nullable QbusCommunication qComm; - private String id; + private Integer id; private double measured = 0.0; private double setpoint = 0.0; - private Integer mode = 0; + private @Nullable Integer mode; private @Nullable QbusThermostatHandler thingHandler; - QbusThermostat(String id) { + QbusThermostat(Integer id) { this.id = id; } - /** - * Update all values of the thermostat - * - * @param measured current temperature in 1°C multiples - * @param setpoint the setpoint temperature in 1°C multiples - * @param mode 0="Manueel", 1="Vorst", 2="Economisch", 3="Comfort", 4="Nacht" - */ - public void updateState(Double measured, Double setpoint, Integer mode) { - setMeasured(measured); - setSetpoint(setpoint); - setMode(mode); - - QbusThermostatHandler handler = thingHandler; - if (handler != null) { - handler.handleStateUpdate(this); - } - } - /** * This method should be called if the ThingHandler for the thing corresponding to the termostat is initialized. * It keeps a record of the thing handler in this object so the thing can be updated when - * the thermostat receives an update from the Qbus IP-interface. + * the thermostat receives an update from the Qbus client. * * @param handler */ @@ -74,9 +54,9 @@ public void setThingHandler(QbusThermostatHandler handler) { } /** - * This method sets a pointer to the qComm object of class {@link QbusCommuncation}. + * This method sets a pointer to the qComm THERMOSTAT of class {@link QbusCommuncation}. * This is then used to be able to call back the sendCommand method in this class to send a command to the - * Qbus IP-interface when.. + * Qbus client. * * @param qComm */ @@ -85,91 +65,78 @@ public void setQComm(QbusCommunication qComm) { } /** - * Get measured temperature. + * Update all values of the Thermostat * - * @return measured temperature in 0.5°C multiples - */ - public double getMeasured() { - return this.measured; - } - - /** - * Set measured temperature. - * - * @param measured + * @param measured current temperature in 1°C multiples + * @param setpoint the setpoint temperature in 1°C multiples + * @param mode 0="Manual", 1="Freeze", 2="Economic", 3="Comfort", 4="Night" */ - private void setMeasured(Double measured) { + public void updateState(Double measured, Double setpoint, Integer mode) { this.measured = measured; - } + this.setpoint = setpoint; + this.mode = mode; - /** - * Get setpoint - * - * @return the setpoint temperature in 0.5°C multiples - */ - public double getSetpoint() { - return this.setpoint; + QbusThermostatHandler handler = this.thingHandler; + if (handler != null) { + handler.handleStateUpdate(this); + } } /** - * Set setpoint temperature. + * Get measured temperature of the Thermostat. * - * @param setpoint + * @return measured temperature in 0.5°C multiples */ - private void setSetpoint(Double setpoint) { - this.setpoint = setpoint; + public @Nullable Double getMeasured() { + return this.measured; } /** - * Get the thermostat mode. + * Get setpoint temperature of the Thermostat. * - * @return the mode: 0="Manueel", 1="Vorst", 2="Economisch", 3="Comfort", 4="Nacht" + * @return the setpoint temperature in 0.5°C multiples */ - public Integer getMode() { - return mode; + public @Nullable Double getSetpoint() { + return this.setpoint; } /** - * Set the thermostat + * Get the Thermostat mode. * - * mode: 0="Manueel", 1="Vorst", 2="Economisch", 3="Comfort", 4="Nacht" + * @return the mode: 0="Manual", 1="Freeze", 2="Economic", 3="Comfort", 4="Night" */ - private void setMode(Integer mode) { - this.mode = mode; + public @Nullable Integer getMode() { + return this.mode; } /** - * Sends thermostat mode to Qbus. + * Sends Thermostat mode to Qbus. * * @param mode * @param sn + * @throws InterruptedException + * @throws IOException */ - public void executeMode(int mode, String sn) { + public void executeMode(int mode, String sn) throws InterruptedException, IOException { QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeThermostat").withId(this.id).withMode(mode); - QbusCommunication comm = qComm; + QbusCommunication comm = this.qComm; if (comm != null) { - try { - comm.sendMessage(qCmd); - } catch (InterruptedException e) { - logger.warn("Could not send command mode for thermostat {}, {}", this.id, e.getMessage()); - } + comm.sendMessage(qCmd); } } /** - * Sends setpoint to Qbus. + * Sends Thermostat setpoint to Qbus. * - * @param d + * @param setpoint + * @throws IOException + * @throws InterruptedException */ - public void executeSetpoint(double d, String sn) { + public void executeSetpoint(double setpoint, String sn) throws InterruptedException, IOException { QbusMessageCmd qCmd = new QbusMessageCmd(sn, "executeThermostat").withId(this.id).withSetPoint(setpoint); - QbusCommunication comm = qComm; + QbusCommunication comm = this.qComm; if (comm != null) { - try { - comm.sendMessage(qCmd); - } catch (InterruptedException e) { - logger.warn("Could not send command setpoitn for thermostat {}, {}", this.id, e.getMessage()); - } + comm.sendMessage(qCmd); } } } diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties index 4368d800c6fd5..924cc7af95275 100644 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties @@ -11,8 +11,8 @@ thing-type.config.qbus.bridge.sn.label = Serienummer van de Controller thing-type.config.qbus.bridge.sn.description = Serienummer van de CTD controller thing-type.config.qbus.bridge.port.label = Poort thing-type.config.qbus.bridge.port.description = Communicatiepoort van de Qbus server (standaard: 8447) -thing-type.config.qbus.bridge.refresh.label = Refresh Timer -thing-type.config.qbus.bridge.refresh.description = Ingestelde timer, bij het verlopen van de timer zal de communicatie met Client/Server gecontroleerd worden en indien nodig herstart. +thing-type.config.qbus.bridge.serverCheck.label = CTD Connectie +thing-type.config.qbus.bridge.serverCheck.description = Ingestelde timer, bij het verlopen van de timer zal de communicatie met de Qbus server gecontroleerd worden en indien nodig herstart. thing-type.qbus.onOff.label = Aan/uit thing-type.qbus.onOff.description = Alle Bistabiel-Mono-Timer-Interval uitgangen diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml index 16c93351e4548..ae4176d26f5f3 100644 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml @@ -24,10 +24,11 @@ 8447 true - - - Refresh interval for connection with Qbus server (min), default 10. If set to 0 or left empty, no - refresh will be scheduled + + + Time to check communication with Qbus Server (min), default 10. If set to 0 or left empty, no + refresh + will be scheduled 10 true From 9b408aeed71b6a091c62f2cf9f97192444311aea Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Sun, 28 Mar 2021 06:59:59 +0200 Subject: [PATCH 16/17] Update requested changes Updates changes as requested by FWolter on 27/03/2021 Signed-off-by: Koen Schockaert --- bundles/org.openhab.binding.qbus/README.md | 16 +++++----- .../qbus/internal/QbusBridgeHandler.java | 29 +++---------------- .../qbus/internal/protocol/QbusCO2.java | 14 --------- .../internal/protocol/QbusCommunication.java | 14 ++++----- .../resources/OH-INF/i18n/qbus_nl.properties | 2 +- .../resources/OH-INF/thing/thing-types.xml | 4 +-- 6 files changed, 22 insertions(+), 57 deletions(-) diff --git a/bundles/org.openhab.binding.qbus/README.md b/bundles/org.openhab.binding.qbus/README.md index a127b23876146..50b5bedccdf19 100644 --- a/bundles/org.openhab.binding.qbus/README.md +++ b/bundles/org.openhab.binding.qbus/README.md @@ -44,19 +44,19 @@ The discovery service is not yet implemented but the System Manager III software ## Bridge configuration ``` -Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, refresh=10 ] { +Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, serverCheck=10 ] { ... } ``` -| Property | Default | Required | Description | -| --------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | -| `addr` | localhost | YES | The ip address of the machine where the Qbus Server runs | -| `sn` | | YES | The serial number of your controller | -| `port` | 8447 | YES | The communication port of the client/server | -| `refresh` | 5 | NO | Refresh time - After x minutes there will be a check if server is still running and if client is still connected. If not - reconnect | +| Property | Default | Required | Description | +| ------------- | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------ | +| `addr` | localhost | YES | The ip address of the machine where the Qbus Server runs | +| `sn` | | YES | The serial number of your controller | +| `port` | 8447 | YES | The communication port of the client/server | +| `serverCheck` | 10 | NO | Refresh time - After x minutes there will be a check if server is still running and if client is still connected. If not - reconnect | @@ -80,7 +80,7 @@ Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, refresh ### Things ``` -Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, refresh=10 ] { +Bridge qbus:bridge:CTD001122 [ addr="localhost", sn="001122", port=8447, serverCheck=10 ] { dimmer 1 "ToonzaalLED" [ dimmerId=100 ] onOff 30 "Toonzaal230V" [ bistabielId=76 ] thermostat 50 "Service" [ thermostatId=99 ] diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java index 30d12112aec82..2f625669ae348 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/QbusBridgeHandler.java @@ -14,8 +14,6 @@ package org.openhab.binding.qbus.internal; import java.io.IOException; -import java.net.InetAddress; -import java.net.UnknownHostException; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -58,31 +56,11 @@ public QbusBridgeHandler(Bridge Bridge) { */ @Override public void initialize() { - readConfig(); - - InetAddress addr; - Integer port = getPort(); Integer serverCheck = getServerCheck(); - if (port == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "No port defined for Qbus Server"); - return; - } + readConfig(); - try { - addr = InetAddress.getByName(getAddress()); - if (addr == null) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "No ip address defined for Qbus Server"); - return; - } else { - createCommunicationObject(addr, port); - } - } catch (UnknownHostException e) { - updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR, - "Incorrect ip address set for Qbus Server"); - } + createCommunicationObject(); if (serverCheck != null) { this.setupRefreshTimer(serverCheck); @@ -105,8 +83,9 @@ private void setBridgeCallBack() { * @param addr : IP address of Qbus server * @param port : Communication port of QbusServer */ - private void createCommunicationObject(InetAddress addr, int port) { + private void createCommunicationObject() { scheduler.submit(() -> { + setQbusCommunication(new QbusCommunication(thing)); QbusCommunication qbusCommunication = getQbusCommunication(); diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java index 9c3276a31edb0..88d0f404925c8 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCO2.java @@ -30,9 +30,6 @@ public final class QbusCO2 { private @Nullable QbusCO2Handler thingHandler; - QbusCO2() { - } - /** * This method should be called if the ThingHandler for the thing corresponding to this CO2 is initialized. * It keeps a record of the thing handler in this object so the thing can be updated when @@ -44,17 +41,6 @@ public void setThingHandler(QbusCO2Handler handler) { this.thingHandler = handler; } - /** - * This method sets a pointer to the qComm CO2 of class {@link QbusCommuncation}. - * This is then used to be able to call back the sendCommand method in this class to send a command to the - * Qbus client. - * - * @param qComm - */ - // public void setQComm(QbusCommunication qComm) { - // this.qComm = qComm; - // } - /** * Get state of CO2. * diff --git a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java index 6df9d47d87d1a..e07032d2ebd6a 100644 --- a/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java +++ b/bundles/org.openhab.binding.qbus/src/main/java/org/openhab/binding/qbus/internal/protocol/QbusCommunication.java @@ -60,13 +60,6 @@ @NonNullByDefault public final class QbusCommunication extends BaseThingHandler { - public QbusCommunication(Thing thing) { - super(thing); - GsonBuilder gsonBuilder = new GsonBuilder(); - gsonBuilder.registerTypeAdapter(QbusMessageBase.class, new QbusMessageDeserializer()); - gsonIn = gsonBuilder.create(); - } - private final Logger logger = LoggerFactory.getLogger(QbusCommunication.class); private @Nullable Socket qSocket; @@ -95,6 +88,13 @@ public QbusCommunication(Thing thing) { private @Nullable QbusBridgeHandler bridgeCallBack; + public QbusCommunication(Thing thing) { + super(thing); + GsonBuilder gsonBuilder = new GsonBuilder(); + gsonBuilder.registerTypeAdapter(QbusMessageBase.class, new QbusMessageDeserializer()); + gsonIn = gsonBuilder.create(); + } + /** * Starts main communication thread. *

      diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties index 924cc7af95275..c1e57f9cd1066 100644 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/i18n/qbus_nl.properties @@ -11,7 +11,7 @@ thing-type.config.qbus.bridge.sn.label = Serienummer van de Controller thing-type.config.qbus.bridge.sn.description = Serienummer van de CTD controller thing-type.config.qbus.bridge.port.label = Poort thing-type.config.qbus.bridge.port.description = Communicatiepoort van de Qbus server (standaard: 8447) -thing-type.config.qbus.bridge.serverCheck.label = CTD Connectie +thing-type.config.qbus.bridge.serverCheck.label = Server Connectie thing-type.config.qbus.bridge.serverCheck.description = Ingestelde timer, bij het verlopen van de timer zal de communicatie met de Qbus server gecontroleerd worden en indien nodig herstart. thing-type.qbus.onOff.label = Aan/uit diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml index ae4176d26f5f3..804616c713ad4 100644 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml @@ -24,11 +24,11 @@ 8447 true - + Time to check communication with Qbus Server (min), default 10. If set to 0 or left empty, no refresh - will be scheduled + willl be scheduled 10 true From ce4611214a7bb812a678120f6e8f1a7f472e5d7c Mon Sep 17 00:00:00 2001 From: QbusKoen Date: Sun, 28 Mar 2021 13:46:43 +0200 Subject: [PATCH 17/17] Update thing-types.xml Update requested as suggested by FWolter on 28/03/2021 Signed-off-by: Koen Schockaert --- .../src/main/resources/OH-INF/thing/thing-types.xml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml index 804616c713ad4..0b8a00cd906b3 100644 --- a/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml +++ b/bundles/org.openhab.binding.qbus/src/main/resources/OH-INF/thing/thing-types.xml @@ -26,9 +26,8 @@ - Time to check communication with Qbus Server (min), default 10. If set to 0 or left empty, no - refresh - willl be scheduled + Time to check communication with Qbus Server (min), default 10. If set to 0 or left empty, no refresh + will be scheduled 10 true

    =|A|Gb&8c0998j4S@nXE671g_hmkY@OP5&N`{5 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¨H8lm6UX%=h9_)Cjk`(2_#z1#4_1YY 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%0DpDPlKZ-uBpk%AvoZ@MC0_~)-b0SJ~=-{4- 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