From c244391d08a69b09df53a9f44c24e9326e51671b Mon Sep 17 00:00:00 2001 From: Michael Barker Date: Thu, 29 Sep 2022 10:23:57 +1300 Subject: [PATCH] [echonetlite] Initial contribution (#11999) * First implementation of Echonet Lite Java Bindings. Only supports Mitsubishi Home Heat Pumps. Signed-off-by: Michael Barker --- CODEOWNERS | 1 + bom/openhab-addons/pom.xml | 5 + .../org.openhab.binding.echonetlite/NOTICE | 13 + .../org.openhab.binding.echonetlite/README.md | 87 ++++ .../org.openhab.binding.echonetlite/pom.xml | 17 + .../src/main/feature/feature.xml | 9 + .../internal/EchonetBridgeConfig.java | 34 ++ .../echonetlite/internal/EchonetChannel.java | 106 ++++ .../echonetlite/internal/EchonetClass.java | 77 +++ .../internal/EchonetClassIndex.java | 39 ++ .../echonetlite/internal/EchonetDevice.java | 211 ++++++++ .../internal/EchonetDeviceConfig.java | 44 ++ .../internal/EchonetDeviceListener.java | 36 ++ .../internal/EchonetDiscoveryListener.java | 23 + .../internal/EchonetDiscoveryService.java | 112 ++++ .../internal/EchonetLiteBindingConstants.java | 46 ++ .../internal/EchonetLiteBridgeHandler.java | 398 ++++++++++++++ .../internal/EchonetLiteHandler.java | 188 +++++++ .../internal/EchonetLiteHandlerFactory.java | 60 +++ .../echonetlite/internal/EchonetMessage.java | 134 +++++ .../internal/EchonetMessageBuilder.java | 103 ++++ .../echonetlite/internal/EchonetObject.java | 200 +++++++ .../internal/EchonetProfileNode.java | 82 +++ .../internal/EchonetPropertyMap.java | 92 ++++ .../binding/echonetlite/internal/Epc.java | 487 ++++++++++++++++++ .../echonetlite/internal/EpcLookupTable.java | 86 ++++ .../binding/echonetlite/internal/Esv.java | 88 ++++ .../binding/echonetlite/internal/HexUtil.java | 71 +++ .../echonetlite/internal/InstanceKey.java | 62 +++ .../echonetlite/internal/LangUtil.java | 42 ++ .../echonetlite/internal/MonotonicClock.java | 33 ++ .../echonetlite/internal/ShortSupplier.java | 24 + .../echonetlite/internal/StateCodec.java | 216 ++++++++ .../echonetlite/internal/StateDecode.java | 28 + .../echonetlite/internal/StateEncode.java | 27 + .../main/resources/OH-INF/binding/binding.xml | 9 + .../OH-INF/i18n/echonetlite.properties | 251 +++++++++ .../resources/OH-INF/thing/channel-types.xml | 378 ++++++++++++++ .../resources/OH-INF/thing/thing-types.xml | 73 +++ .../binding/echonetlite/internal/EpcTest.java | 43 ++ .../internal/protocol/LangUtilTest.java | 31 ++ .../internal/protocol/StateCodecTest.java | 130 +++++ bundles/pom.xml | 1 + 43 files changed, 4197 insertions(+) create mode 100644 bundles/org.openhab.binding.echonetlite/NOTICE create mode 100644 bundles/org.openhab.binding.echonetlite/README.md create mode 100644 bundles/org.openhab.binding.echonetlite/pom.xml create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/feature/feature.xml create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetBridgeConfig.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetChannel.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetClass.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetClassIndex.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDevice.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDeviceConfig.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDeviceListener.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDiscoveryListener.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDiscoveryService.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetLiteBindingConstants.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetLiteBridgeHandler.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetLiteHandler.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetLiteHandlerFactory.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetMessage.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetMessageBuilder.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetObject.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetProfileNode.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetPropertyMap.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/Epc.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EpcLookupTable.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/Esv.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/HexUtil.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/InstanceKey.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/LangUtil.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/MonotonicClock.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/ShortSupplier.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/StateCodec.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/StateDecode.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/StateEncode.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/resources/OH-INF/binding/binding.xml create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/resources/OH-INF/i18n/echonetlite.properties create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/resources/OH-INF/thing/channel-types.xml create mode 100644 bundles/org.openhab.binding.echonetlite/src/main/resources/OH-INF/thing/thing-types.xml create mode 100644 bundles/org.openhab.binding.echonetlite/src/test/java/org/openhab/binding/echonetlite/internal/EpcTest.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/test/java/org/openhab/binding/echonetlite/internal/protocol/LangUtilTest.java create mode 100644 bundles/org.openhab.binding.echonetlite/src/test/java/org/openhab/binding/echonetlite/internal/protocol/StateCodecTest.java diff --git a/CODEOWNERS b/CODEOWNERS index 32edcbe2cda..cf6bc84ef17 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -77,6 +77,7 @@ /bundles/org.openhab.binding.dsmr/ @Hilbrand /bundles/org.openhab.binding.dwdpollenflug/ @DerOetzi /bundles/org.openhab.binding.dwdunwetter/ @limdul79 +/bundles/org.openhab.binding.echonetlite/ @mikeb01 /bundles/org.openhab.binding.ecobee/ @mhilbush /bundles/org.openhab.binding.ecotouch/ @sibbi77 /bundles/org.openhab.binding.ecowatt/ @lolodomo diff --git a/bom/openhab-addons/pom.xml b/bom/openhab-addons/pom.xml index 73ebdac2488..be5f6fa7856 100644 --- a/bom/openhab-addons/pom.xml +++ b/bom/openhab-addons/pom.xml @@ -381,6 +381,11 @@ org.openhab.binding.easee ${project.version} + + org.openhab.addons.bundles + org.openhab.binding.echonetlite + ${project.version} + org.openhab.addons.bundles org.openhab.binding.ecobee diff --git a/bundles/org.openhab.binding.echonetlite/NOTICE b/bundles/org.openhab.binding.echonetlite/NOTICE new file mode 100644 index 00000000000..38d625e3492 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/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.echonetlite/README.md b/bundles/org.openhab.binding.echonetlite/README.md new file mode 100644 index 00000000000..fd4da57d6ae --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/README.md @@ -0,0 +1,87 @@ +# EchonetLite Binding + +This binding supports devices that make use of the Echonet Lite specification (https://echonet.jp/spec_v113_lite_en/). + +## Supported Things + +* Mitsubishi Electric MAC-568IF-E Wi-Fi interface (common on most Mitsubishi Heat Pumps). + +## Discovery + +Discovery is supported using UDP Multicast. +When running over Wi-Fi it is advisable to run openHAB on the same network as the Echonet Lite devices. +Multicast traffic doesn't easily route over multiple networks and will often be dropped. +Discovery is handled via the Echonet Lite bridge, which contains the configuration of the multicast address used for discovery and asynchronous device notifications along with the port. +It is unlikely that this configuration will require changing. + +## Bridge Configuration + +The bridge configuration defaults should be applicable in most scenarios. +If device discovery is not working, this is most likely caused by the inability to receive multicast traffic from the device nodes. + +* __port__: Port used for messaging both to and from device nodes, defaults to 3610. +* __multicastAddress__: Multicast address used to discover device nodes and to receive asynchronous notifications from devices. + +## Thing Configuration + +* __hostname__: Hostname or IP address of the device node. +* __port__: Port used to communicate with the device. +* __groupCode__: Group code as specified in "APPENDIX Detailed Requirements for ECHONET Device objects" (https://echonet.jp/spec_object_rp1_en/). +For Air Conditioners the value is '1'. +* __classCode__: Class code for the device, see __groupCode__ for reference information. +The value for Home Air Conditioners is '48' (0x30). +* __instance__: Instance identifier if multiple instances are running on the same IP address. +Typically, this value will be '1'. +* __pollIntervalMs__: Interval between polls of the device for its current status. +If multicast is not working this will determine the latency at which changes made directly on the device will be propagated back to openHAB, default is 30 000ms. +* __retryTimeoutMs__: Length of time the bridge will wait before resubmitting a request, default is 2 000ms. + +Because the binding uses UDP, packets can be lost on the network, so retries are necessary. +Testing has shown that 2 000ms is a reasonable default that allows for timely retries without rejecting slow, but legitimate responses. + +## Channels + +Channels are derived from the Echonet Lite specification and vary from device to device depending on capabilities. +The full set of potential channels is available from "APPENDIX Detailed Requirements for ECHONET Device objects" (https://echonet.jp/spec_object_rp1_en/) + +The channels currently implemented are: + +| Channel | Data Type | Description | +|------------------------------------|-----------|-------------------------------------------------------------------------| +| operationStatus | Switch | Switch On/Off the device | +| installationLocation | String | Installation location (option) | +| standardVersionInformation | String | Standard Version Information | +| identificationNumber | String | Unique id for device (used by auto discovery for the thingId) | +| manufacturerFaultCode | String | Manufacturer Fault Code | +| faultStatus | Switch | Fault Status | +| faultDescription | String | Fault Description | +| manufacturerCode | String | Manufacturer Code | +| businessFacilityCode | String | Business Facility Code | +| powerSavingOperationSetting | Switch | Controls whether the unit is in power saving operation or not | +| cumulativeOperatingTime | Number | Cumulative Operating Time | +| airFlowRate | String | Air Flow Rate | +| automaticControlOfAirFlowDirection | String | The type of automatic control applied to the air flow direction, if any | +| automaticSwingOfAirFlow | String | Automatic Swing Of Air Flow | +| airFlowDirectionVertical | String | Air Flow Direction Vertical | +| airFlowDirectionHorizontal | String | Air Flow Direction Horizontal | +| operationMode | String | The current mode for the Home AC unit (heating, cooling, etc.) | +| setTemperature | Number | Desired target room temperature | +| measuredRoomTemperature | Number | Measured Room Temperature | +| measuredOutdoorTemperature | Number | Measured Outdoor Temperature | + +## Full Example + + +### Things + +``` +Bridge echonetlite:bridge:1 [port="3610", multicastAddress="224.0.23.0"] { + Thing device HeatPump_Bedroom1 "HeatPump Bedroom 1" @ "Bedroom 1" [hostname="192.168.0.55", port="3610", groupCode="1", classCode="48", instance="1", pollIntervalMs="30000", retryTimeoutMs="2000"] +} +``` + +### Items + +``` +Switch HeatPumpBedroom1_OperationStatus "HeatPump Bedroom1 Operation Status" {channel="echonetlite:device:1:HeatPump_Bedroom1:operationStatus"} +``` diff --git a/bundles/org.openhab.binding.echonetlite/pom.xml b/bundles/org.openhab.binding.echonetlite/pom.xml new file mode 100644 index 00000000000..20a4db3e93e --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/pom.xml @@ -0,0 +1,17 @@ + + + + 4.0.0 + + + org.openhab.addons.bundles + org.openhab.addons.reactor.bundles + 3.4.0-SNAPSHOT + + + org.openhab.binding.echonetlite + + openHAB Add-ons :: Bundles :: EchonetLite Binding + + diff --git a/bundles/org.openhab.binding.echonetlite/src/main/feature/feature.xml b/bundles/org.openhab.binding.echonetlite/src/main/feature/feature.xml new file mode 100644 index 00000000000..9510220925d --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/feature/feature.xml @@ -0,0 +1,9 @@ + + + mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features + + + openhab-runtime-base + mvn:org.openhab.addons.bundles/org.openhab.binding.echonetlite/${project.version} + + diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetBridgeConfig.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetBridgeConfig.java new file mode 100644 index 00000000000..ad916b7cddf --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetBridgeConfig.java @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link EchonetBridgeConfig} class contains fields mapping thing configuration parameters. + * + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public class EchonetBridgeConfig { + + @Nullable + public String multicastAddress; + public int port; + + @Override + public String toString() { + return "EchonetBridgeConfig{" + "multicastAddress='" + multicastAddress + '\'' + ", port=" + port + '}'; + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetChannel.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetChannel.java new file mode 100644 index 00000000000..51d08c41cad --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetChannel.java @@ -0,0 +1,106 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import java.io.IOException; +import java.net.Inet4Address; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketAddress; +import java.net.StandardProtocolFamily; +import java.nio.ByteBuffer; +import java.nio.channels.DatagramChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.util.Enumeration; +import java.util.function.BiConsumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Wraps a Datagram channel for sending/receiving data to/from echonet lite devices. + * + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public class EchonetChannel { + + private final Logger logger = LoggerFactory.getLogger(EchonetChannel.class); + + private final DatagramChannel channel; + private final Selector selector = Selector.open(); + + private short tid = 0; + + public EchonetChannel(InetSocketAddress discoveryAddress) throws IOException { + channel = DatagramChannel.open(StandardProtocolFamily.INET); + channel.bind(new InetSocketAddress("0.0.0.0", discoveryAddress.getPort())); + final Enumeration networkInterfaces = NetworkInterface.getNetworkInterfaces(); + while (networkInterfaces.hasMoreElements()) { + final NetworkInterface networkInterface = (NetworkInterface) networkInterfaces.nextElement(); + if (networkInterface.supportsMulticast() && hasIpV4Address(networkInterface)) { + channel.join(discoveryAddress.getAddress(), networkInterface); + } + } + channel.configureBlocking(false); + channel.register(selector, SelectionKey.OP_READ); + } + + private boolean hasIpV4Address(final NetworkInterface networkInterface) { + return networkInterface.inetAddresses().anyMatch(ia -> ia instanceof Inet4Address); + } + + public void close() { + try { + logger.debug("closing selector"); + selector.close(); + logger.debug("closing channel"); + channel.close(); + } catch (IOException ignore) { + } + } + + short nextTid() { + return tid++; + } + + public void sendMessage(EchonetMessageBuilder messageBuilder) throws IOException { + messageBuilder.buffer().flip(); + channel.send(messageBuilder.buffer(), messageBuilder.address()); + } + + public void pollMessages(EchonetMessage echonetMessage, BiConsumer consumer, + final long timeout) throws IOException { + selector.select(selectionKey -> { + final DatagramChannel channel = (DatagramChannel) selectionKey.channel(); + try { + final ByteBuffer buffer = echonetMessage.bufferForRead(); + final SocketAddress address = channel.receive(buffer); + + echonetMessage.sourceAddress(address); + buffer.flip(); + long t0 = System.currentTimeMillis(); + consumer.accept(echonetMessage, address); + long t1 = System.currentTimeMillis(); + final long processingTimeMs = t1 - t0; + if (500 < processingTimeMs) { + logger.debug("Message took {}ms to process", processingTimeMs); + } + } catch (IOException e) { + logger.warn("Failed to receive on channel", e); + } + }, timeout); + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetClass.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetClass.java new file mode 100644 index 00000000000..5a4e4dd3c2b --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetClass.java @@ -0,0 +1,77 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public enum EchonetClass { + AIRCON_HOMEAC(0x01, 0x30, (Epc[]) Epc.Device.values(), (Epc[]) Epc.AcGroup.values(), (Epc[]) Epc.HomeAc.values()), + MANAGEMENT_CONTROLLER(0x05, 0xFF, new Epc[0], new Epc[0], new Epc[0]), + NODE_PROFILE(0x0e, 0xf0, (Epc[]) Epc.Profile.values(), (Epc[]) Epc.ProfileGroup.values(), + (Epc[]) Epc.NodeProfile.values()); + + private final int groupCode; + private final int classCode; + private final Epc[] deviceProperties; + private final Epc[] groupProperties; + private final Epc[] classProperties; + + EchonetClass(final int groupCode, final int classCode, Epc[] deviceProperties, Epc[] groupProperties, + Epc[] classProperties) { + this.groupCode = groupCode; + this.classCode = classCode; + this.deviceProperties = deviceProperties; + this.groupProperties = groupProperties; + this.classProperties = classProperties; + } + + public static EchonetClass resolve(final int groupCode, final int classCode) { + final EchonetClass[] values = values(); + for (EchonetClass value : values) { + if (value.groupCode == groupCode && value.classCode == classCode) { + return value; + } + } + + throw new IllegalArgumentException("Unable to find class: " + groupCode + "/" + classCode); + } + + public int groupCode() { + return groupCode; + } + + public int classCode() { + return classCode; + } + + Epc[] deviceProperties() { + return deviceProperties; + } + + Epc[] groupProperties() { + return groupProperties; + } + + Epc[] classProperties() { + return classProperties; + } + + public String toString() { + return name() + "{" + "groupCode=0x" + Integer.toHexString(groupCode) + ", classCode=0x" + + Integer.toHexString(0xFF & classCode) + '}'; + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetClassIndex.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetClassIndex.java new file mode 100644 index 00000000000..95c48f66ddb --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetClassIndex.java @@ -0,0 +1,39 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public enum EchonetClassIndex { + INSTANCE; + + private static final EchonetClass[] INDEX = new EchonetClass[1 << 16]; + static { + final EchonetClass[] values = EchonetClass.values(); + for (final EchonetClass value : values) { + INDEX[codeToIndex(value.groupCode(), value.classCode())] = value; + } + } + + public static int codeToIndex(final int groupCode, final int classCode) { + return ((0xFF & groupCode) << 8) + (0xFF & classCode); + } + + public EchonetClass lookup(final int groupCode, final int classCode) { + return INDEX[codeToIndex(groupCode, classCode)]; + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDevice.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDevice.java new file mode 100644 index 00000000000..7dcf22ec6cf --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDevice.java @@ -0,0 +1,211 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import static org.openhab.binding.echonetlite.internal.HexUtil.hex; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public class EchonetDevice extends EchonetObject { + + private final LinkedHashMap pendingSets = new LinkedHashMap<>(); + private final HashMap stateFields = new HashMap<>(); + private final HashMap epcByChannelId = new HashMap<>(); + private final Logger logger = LoggerFactory.getLogger(EchonetDevice.class); + @Nullable + private EchonetPropertyMap getPropertyMap; + private EchonetDeviceListener listener; + private boolean initialised = false; + + private long lastPollMs = 0; + + public EchonetDevice(final InstanceKey instanceKey, EchonetDeviceListener listener) { + super(instanceKey, Epc.Device.GET_PROPERTY_MAP); + this.listener = listener; + } + + public void applyProperty(InstanceKey sourceInstanceKey, Esv esv, final int epcCode, final int pdc, + final ByteBuffer edt) { + final Epc epc = Epc.lookup(instanceKey().klass.groupCode(), instanceKey().klass.classCode(), epcCode); + + if ((Esv.Get_Res == esv || Esv.Get_SNA == esv || Esv.INF == esv) && 0 < pdc) { + pendingGets.remove(epc); + + int edtPosition = edt.position(); + + final StateDecode decoder = epc.decoder(); + State state = null; + if (null != decoder) { + state = decoder.decodeState(edt); + if (null == stateFields.put(epc, state)) { + epcByChannelId.put(epc.channelId(), epc); + } + + final @Nullable State pendingState = lookupPendingSet(epc); + if (null != pendingState && pendingState.equals(state)) { + logger.debug("pendingSet - removing: {} {}", epc, state); + pendingSets.remove(epc); + } else if (null != pendingState) { + logger.debug("pendingSet - state mismatch: {} {} {}", epc, pendingState, state); + } + + if (initialised) { + listener.onUpdated(epc.channelId(), state); + } + } else if (Epc.Device.GET_PROPERTY_MAP == epc) { + if (null == getPropertyMap) { + final EchonetPropertyMap getPropertyMap = new EchonetPropertyMap(epc); + getPropertyMap.update(edt); + getPropertyMap.getProperties(instanceKey().klass.groupCode(), instanceKey().klass.classCode(), + Set.of(Epc.Device.GET_PROPERTY_MAP), pendingGets); + this.getPropertyMap = getPropertyMap; + } + } + + if (!initialised && null != getPropertyMap && pendingGets.isEmpty()) { + initialised = true; + listener.onInitialised(identifier(), instanceKey, channelIds()); + stateFields.forEach((e, s) -> listener.onUpdated(e.channelId(), s)); + } + + if (logger.isDebugEnabled()) { + String value = null != state ? state.toString() : ""; + edt.position(edtPosition); + logger.debug("Applying: {}({},{}) {} {} pending: {}", epc, hex(epc.code()), pdc, value, hex(edt), + pendingGets.size()); + } + } else if (esv == Esv.Set_Res) { + pendingSets.remove(epc); + } + } + + public String identifier() { + final State identificationNumber = stateFields.get(Epc.Device.IDENTIFICATION_NUMBER); + if (null == identificationNumber) { + throw new IllegalStateException("Echonet devices must support identification number property"); + } + + return identificationNumber.toString(); + } + + public boolean buildUpdateMessage(final EchonetMessageBuilder messageBuilder, final ShortSupplier tidSupplier, + final long nowMs, InstanceKey managementControllerKey) { + if (pendingSets.isEmpty()) { + return false; + } + + final InflightRequest inflightSetRequest = this.inflightSetRequest; + + if (hasInflight(nowMs, inflightSetRequest)) { + return false; + } + + final short tid = tidSupplier.getAsShort(); + messageBuilder.start(tid, managementControllerKey, instanceKey, Esv.SetC); + + pendingSets.forEach((k, v) -> { + final StateEncode encoder = k.encoder(); + if (null != encoder) { + final ByteBuffer buffer = messageBuilder.edtBuffer(); + encoder.encodeState(v, buffer); + messageBuilder.appendEpcUpdate(k.code(), buffer.flip()); + } + }); + + inflightSetRequest.requestSent(tid, nowMs); + + return true; + } + + public void update(String channelId, State state) { + final Epc epc = epcByChannelId.get(channelId); + if (null == epc) { + logger.warn("Unable to find epc for channelId: {}", channelId); + return; + } + + pendingSets.put(epc, state); + } + + @Override + public void removed() { + listener.onRemoved(); + } + + public void checkTimeouts() { + if (EchonetLiteBindingConstants.OFFLINE_TIMEOUT_COUNT <= inflightGetRequest.timeoutCount()) { + listener.onOffline(); + } + } + + public void refreshAll(long nowMs) { + final EchonetPropertyMap getPropertyMap = this.getPropertyMap; + if (lastPollMs + pollIntervalMs <= nowMs && null != getPropertyMap) { + getPropertyMap.getProperties(instanceKey().klass.groupCode(), instanceKey().klass.classCode(), + Set.of(Epc.Device.GET_PROPERTY_MAP), pendingGets); + lastPollMs = nowMs; + } + } + + @Override + public void refresh(String channelId) { + final Epc epc = epcByChannelId.get(channelId); + if (null == epc) { + return; + } + + final State state = stateFields.get(epc); + if (null == state) { + return; + } + + listener.onUpdated(channelId, state); + } + + public void setListener(EchonetDeviceListener listener) { + this.listener = listener; + if (initialised) { + listener.onInitialised(identifier(), instanceKey(), channelIds()); + stateFields.forEach((e, s) -> listener.onUpdated(e.channelId(), s)); + } + } + + private Map channelIds() { + final HashMap channelIdAndType = new HashMap<>(); + for (Epc e : stateFields.keySet()) { + final StateDecode decoder = e.decoder(); + if (null != decoder) { + channelIdAndType.put(e.channelId(), decoder.itemType()); + } + } + return channelIdAndType; + } + + private @Nullable State lookupPendingSet(Epc epc) { + return pendingSets.get(epc); + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDeviceConfig.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDeviceConfig.java new file mode 100644 index 00000000000..2657ff40261 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDeviceConfig.java @@ -0,0 +1,44 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * The {@link EchonetDeviceConfig} class contains fields mapping thing configuration parameters. + * + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public class EchonetDeviceConfig { + + /** + * Sample configuration parameters. Replace with your own. + */ + @Nullable + public String hostname; + public int port; + public int groupCode; + public int classCode; + public int instance; + public long pollIntervalMs; + public long retryTimeoutMs; + + @Override + public String toString() { + return "EchonetLiteConfiguration{" + "hostname='" + hostname + '\'' + ", port=" + port + ", groupCode=" + + groupCode + ", classCode=" + classCode + ", instance=" + instance + ", pollIntervalMs=" + + pollIntervalMs + ", retryTimeoutMs=" + retryTimeoutMs + '}'; + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDeviceListener.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDeviceListener.java new file mode 100644 index 00000000000..1d6fb43025f --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDeviceListener.java @@ -0,0 +1,36 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.types.State; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public interface EchonetDeviceListener { + default void onInitialised(String identifier, InstanceKey instanceKey, Map channelIdAndType) { + } + + default void onUpdated(String channelId, State value) { + } + + default void onRemoved() { + } + + default void onOffline() { + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDiscoveryListener.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDiscoveryListener.java new file mode 100644 index 00000000000..3769bbe25fb --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDiscoveryListener.java @@ -0,0 +1,23 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public interface EchonetDiscoveryListener { + void onDeviceFound(String identifier, InstanceKey instanceKey); +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDiscoveryService.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDiscoveryService.java new file mode 100644 index 00000000000..a7692f1f2a9 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetDiscoveryService.java @@ -0,0 +1,112 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import static org.openhab.binding.echonetlite.internal.EchonetLiteBindingConstants.PROPERTY_NAME_CLASS_CODE; +import static org.openhab.binding.echonetlite.internal.EchonetLiteBindingConstants.PROPERTY_NAME_GROUP_CODE; +import static org.openhab.binding.echonetlite.internal.EchonetLiteBindingConstants.PROPERTY_NAME_HOSTNAME; +import static org.openhab.binding.echonetlite.internal.EchonetLiteBindingConstants.PROPERTY_NAME_INSTANCE; +import static org.openhab.binding.echonetlite.internal.EchonetLiteBindingConstants.PROPERTY_NAME_INSTANCE_KEY; +import static org.openhab.binding.echonetlite.internal.EchonetLiteBindingConstants.PROPERTY_NAME_PORT; +import static org.openhab.binding.echonetlite.internal.EchonetLiteBindingConstants.THING_TYPE_ECHONET_DEVICE; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.config.discovery.AbstractDiscoveryService; +import org.openhab.core.config.discovery.DiscoveryResult; +import org.openhab.core.config.discovery.DiscoveryResultBuilder; +import org.openhab.core.thing.ThingUID; +import org.openhab.core.thing.binding.ThingHandler; +import org.openhab.core.thing.binding.ThingHandlerService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public class EchonetDiscoveryService extends AbstractDiscoveryService + implements EchonetDiscoveryListener, ThingHandlerService { + + private final Logger logger = LoggerFactory.getLogger(EchonetDiscoveryService.class); + + @Nullable + private EchonetLiteBridgeHandler bridgeHandler; + + public EchonetDiscoveryService() { + super(Set.of(THING_TYPE_ECHONET_DEVICE), 10); + } + + @Override + protected void startScan() { + final EchonetLiteBridgeHandler bridgeHandler = this.bridgeHandler; + logger.debug("startScan: {}", bridgeHandler); + if (null != bridgeHandler) { + bridgeHandler.startDiscovery(this); + } + } + + @Override + protected synchronized void stopScan() { + final EchonetLiteBridgeHandler bridgeHandler = this.bridgeHandler; + logger.debug("stopScan: {}", bridgeHandler); + if (null != bridgeHandler) { + bridgeHandler.stopDiscovery(); + } + } + + @Override + public void onDeviceFound(String identifier, InstanceKey instanceKey) { + final EchonetLiteBridgeHandler bridgeHandler = this.bridgeHandler; + + if (null == bridgeHandler) { + return; + } + + final DiscoveryResult discoveryResult = DiscoveryResultBuilder + .create(new ThingUID(THING_TYPE_ECHONET_DEVICE, bridgeHandler.getThing().getUID(), identifier)) + .withProperty(PROPERTY_NAME_INSTANCE_KEY, instanceKey.representationProperty()) + .withProperty(PROPERTY_NAME_HOSTNAME, instanceKey.address.getAddress().getHostAddress()) + .withProperty(PROPERTY_NAME_PORT, instanceKey.address.getPort()) + .withProperty(PROPERTY_NAME_GROUP_CODE, instanceKey.klass.groupCode()) + .withProperty(PROPERTY_NAME_CLASS_CODE, instanceKey.klass.classCode()) + .withProperty(PROPERTY_NAME_INSTANCE, instanceKey.instance) + .withBridge(bridgeHandler.getThing().getUID()).withRepresentationProperty(PROPERTY_NAME_INSTANCE_KEY) + .build(); + thingDiscovered(discoveryResult); + } + + @Override + public void deactivate() { + ThingHandlerService.super.deactivate(); + } + + @Override + public void activate() { + ThingHandlerService.super.activate(); + } + + @Override + public void setThingHandler(ThingHandler thingHandler) { + if (thingHandler instanceof EchonetLiteBridgeHandler) { + this.bridgeHandler = (EchonetLiteBridgeHandler) thingHandler; + } + } + + @Override + public @Nullable ThingHandler getThingHandler() { + return bridgeHandler; + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetLiteBindingConstants.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetLiteBindingConstants.java new file mode 100644 index 00000000000..9060600a2ee --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetLiteBindingConstants.java @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.thing.ThingTypeUID; + +/** + * The {@link EchonetLiteBindingConstants} class defines common constants, which are + * used across the whole binding. + * + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public class EchonetLiteBindingConstants { + + public static final long DEFAULT_POLL_INTERVAL_MS = 30_000; + public static final long DEFAULT_RETRY_TIMEOUT_MS = 2_000; + public static final int NETWORK_WAIT_TIMEOUT = 250; + + // List of all Thing Type UIDs + public static final String BINDING_ID = "echonetlite"; + public static final ThingTypeUID THING_TYPE_ECHONET_DEVICE = new ThingTypeUID(BINDING_ID, "device"); + public static final ThingTypeUID THING_TYPE_ECHONET_BRIDGE = new ThingTypeUID(BINDING_ID, "bridge"); + + public static final StateCodec.OnOffCodec ON_OFF_CODEC_30_31 = new StateCodec.OnOffCodec(0x30, 0x31); + public static final StateCodec.OnOffCodec ON_OFF_CODEC_41_42 = new StateCodec.OnOffCodec(0x41, 0x42); + + public static final String PROPERTY_NAME_INSTANCE_KEY = "instanceKey"; + public static final String PROPERTY_NAME_HOSTNAME = "hostname"; + public static final String PROPERTY_NAME_PORT = "port"; + public static final String PROPERTY_NAME_GROUP_CODE = "groupCode"; + public static final String PROPERTY_NAME_CLASS_CODE = "classCode"; + public static final String PROPERTY_NAME_INSTANCE = "instance"; + public static final int OFFLINE_TIMEOUT_COUNT = 2; +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetLiteBridgeHandler.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetLiteBridgeHandler.java new file mode 100644 index 00000000000..4e6f03639f4 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetLiteBridgeHandler.java @@ -0,0 +1,398 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import static java.util.Objects.requireNonNull; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +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.thing.binding.ThingHandlerService; +import org.openhab.core.types.Command; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Bridge handler for echonet lite devices. By default, all messages (inbound and outbound) happen on port 3610, so + * we can only have a single listener for echonet lite messages. Hence, using a bridge model to handle communications + * and discovery. + * + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public class EchonetLiteBridgeHandler extends BaseBridgeHandler { + + private final Logger logger = LoggerFactory.getLogger(EchonetLiteBridgeHandler.class); + private final ArrayBlockingQueue requests = new ArrayBlockingQueue<>(1024); + private final Map devicesByKey = new HashMap<>(); + private final EchonetMessageBuilder messageBuilder = new EchonetMessageBuilder(); + private final Thread networkingThread = new Thread(this::poll); + private final EchonetMessage echonetMessage = new EchonetMessage(); + private final MonotonicClock clock = new MonotonicClock(); + + @Nullable + private EchonetChannel echonetChannel; + + @Nullable + private InstanceKey managementControllerKey; + + @Nullable + private InstanceKey discoveryKey; + + public EchonetLiteBridgeHandler(Bridge bridge) { + super(bridge); + } + + private void start(final InstanceKey managementControllerKey, InstanceKey discoveryKey) throws IOException { + this.managementControllerKey = managementControllerKey; + this.discoveryKey = discoveryKey; + + logger.debug("Binding echonet channel"); + echonetChannel = new EchonetChannel(discoveryKey.address); + logger.debug("Starting networking thread"); + + networkingThread.setName("OH-binding-" + EchonetLiteBindingConstants.BINDING_ID); + networkingThread.setDaemon(true); + networkingThread.start(); + } + + public void newDevice(InstanceKey instanceKey, long pollIntervalMs, long retryTimeoutMs, + final EchonetDeviceListener echonetDeviceListener) { + requests.add(new NewDeviceMessage(instanceKey, pollIntervalMs, retryTimeoutMs, echonetDeviceListener)); + } + + private void newDeviceInternal(final NewDeviceMessage message) { + final EchonetObject echonetObject = devicesByKey.get(message.instanceKey); + if (null != echonetObject) { + if (echonetObject instanceof EchonetDevice) { + logger.debug("Update item: {} already discovered", message.instanceKey); + EchonetDevice device = (EchonetDevice) echonetObject; + device.setTimeouts(message.pollIntervalMs, message.retryTimeoutMs); + device.setListener(message.echonetDeviceListener); + } else { + logger.debug("Item: {} already discovered, but was not a device", message.instanceKey); + } + } else { + logger.debug("New Device: {}", message.instanceKey); + final EchonetDevice device = new EchonetDevice(message.instanceKey, message.echonetDeviceListener); + device.setTimeouts(message.pollIntervalMs, message.retryTimeoutMs); + devicesByKey.put(message.instanceKey, device); + } + } + + public void refreshDevice(final InstanceKey instanceKey, final String channelId) { + requests.add(new RefreshMessage(instanceKey, channelId)); + } + + private void refreshDeviceInternal(final RefreshMessage refreshMessage) { + final EchonetObject item = devicesByKey.get(refreshMessage.instanceKey); + if (null != item) { + item.refresh(refreshMessage.channelId); + } + } + + public void removeDevice(final InstanceKey instanceKey) { + requests.add(new RemoveDevice(instanceKey)); + } + + private void removeDeviceInternal(final RemoveDevice removeDevice) { + final EchonetObject remove = devicesByKey.remove(removeDevice.instanceKey); + + logger.debug("Removing device: {}, {}", removeDevice.instanceKey, remove); + if (null != remove) { + remove.removed(); + } + } + + public void updateDevice(final InstanceKey instanceKey, final String id, final State command) { + requests.add(new UpdateDevice(instanceKey, id, command)); + } + + public void updateDeviceInternal(UpdateDevice updateDevice) { + final EchonetObject echonetObject = devicesByKey.get(updateDevice.instanceKey); + + if (null == echonetObject) { + logger.warn("Device not found for update: {}", updateDevice); + return; + } + + echonetObject.update(updateDevice.channelId, updateDevice.state); + } + + public void startDiscovery(EchonetDiscoveryListener echonetDiscoveryListener) { + requests.offer(new StartDiscoveryMessage(echonetDiscoveryListener, requireNonNull(discoveryKey))); + } + + public void startDiscoveryInternal(StartDiscoveryMessage startDiscovery) { + devicesByKey.put(startDiscovery.instanceKey, new EchonetProfileNode(startDiscovery.instanceKey, + this::onDiscoveredInstanceKey, startDiscovery.echonetDiscoveryListener)); + } + + public void stopDiscovery() { + requests.offer(new StopDiscoveryMessage(requireNonNull(discoveryKey))); + } + + private void stopDiscoveryInternal(StopDiscoveryMessage stopDiscovery) { + devicesByKey.remove(stopDiscovery.instanceKey); + } + + private void onDiscoveredInstanceKey(EchonetDevice device) { + if (null == devicesByKey.putIfAbsent(device.instanceKey(), device)) { + logger.debug("New device discovered: {}", device.instanceKey); + } + } + + private void pollDevices(long nowMs, EchonetChannel echonetChannel) { + for (EchonetObject echonetObject : devicesByKey.values()) { + if (echonetObject.buildUpdateMessage(messageBuilder, echonetChannel::nextTid, nowMs, + requireNonNull(managementControllerKey))) { + try { + echonetChannel.sendMessage(messageBuilder); + } catch (IOException e) { + logger.warn("Failed to send echonet message", e); + } + } + + echonetObject.refreshAll(nowMs); + + if (echonetObject.buildPollMessage(messageBuilder, echonetChannel::nextTid, nowMs, + requireNonNull(managementControllerKey))) { + try { + echonetChannel.sendMessage(messageBuilder); + } catch (IOException e) { + logger.warn("Failed to send echonet message", e); + } + } else { + echonetObject.checkTimeouts(); + } + } + } + + private void pollRequests() { + Message message; + while (null != (message = requestsPoll())) { + logger.debug("Received request: {}", message); + if (message instanceof NewDeviceMessage) { + newDeviceInternal((NewDeviceMessage) message); + } else if (message instanceof RefreshMessage) { + refreshDeviceInternal((RefreshMessage) message); + } else if (message instanceof RemoveDevice) { + removeDeviceInternal((RemoveDevice) message); + } else if (message instanceof UpdateDevice) { + updateDeviceInternal((UpdateDevice) message); + } else if (message instanceof StartDiscoveryMessage) { + startDiscoveryInternal((StartDiscoveryMessage) message); + } else if (message instanceof StopDiscoveryMessage) { + stopDiscoveryInternal((StopDiscoveryMessage) message); + } + } + } + + private @Nullable Message requestsPoll() { + return requests.poll(); + } + + private void pollNetwork(EchonetChannel echonetChannel) { + try { + echonetChannel.pollMessages(echonetMessage, this::onMessage, + EchonetLiteBindingConstants.NETWORK_WAIT_TIMEOUT); + } catch (IOException e) { + logger.warn("Failed to poll for messages", e); + } + } + + private void onMessage(final EchonetMessage echonetMessage, final SocketAddress sourceAddress) { + final EchonetClass echonetClass = echonetMessage.sourceClass(); + if (null == echonetClass) { + logger.warn("Unable to find echonetClass for message: {}, from: {}", echonetMessage.toDebug(), + sourceAddress); + return; + } + + final InstanceKey instanceKey = new InstanceKey((InetSocketAddress) sourceAddress, echonetClass, + echonetMessage.instance()); + final Esv esv = echonetMessage.esv(); + + EchonetObject echonetObject = devicesByKey.get(instanceKey); + if (null == echonetObject) { + echonetObject = devicesByKey.get(discoveryKey); + } + + logger.debug("Message {} for: {}", esv, echonetObject); + if (null != echonetObject) { + echonetObject.applyHeader(esv, echonetMessage.tid(), clock.timeMs()); + while (echonetMessage.moveNext()) { + final int epc = echonetMessage.currentEpc(); + final int pdc = echonetMessage.currentPdc(); + ByteBuffer edt = echonetMessage.currentEdt(); + echonetObject.applyProperty(instanceKey, esv, epc, pdc, edt); + } + } + } + + private void poll() { + try { + doPoll(); + updateStatus(ThingStatus.ONLINE); + + while (!Thread.currentThread().isInterrupted()) { + doPoll(); + } + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage()); + } + } + + private void doPoll() { + final long nowMs = clock.timeMs(); + pollRequests(); + pollDevices(nowMs, requireNonNull(echonetChannel)); + pollNetwork(requireNonNull(echonetChannel)); + } + + @Override + public void initialize() { + final EchonetBridgeConfig bridgeConfig = getConfigAs(EchonetBridgeConfig.class); + + final InstanceKey managementControllerKey = new InstanceKey(new InetSocketAddress(bridgeConfig.port), + EchonetClass.MANAGEMENT_CONTROLLER, (byte) 0x01); + final InstanceKey discoveryKey = new InstanceKey( + new InetSocketAddress(requireNonNull(bridgeConfig.multicastAddress), bridgeConfig.port), + EchonetClass.NODE_PROFILE, (byte) 0x01); + + updateStatus(ThingStatus.UNKNOWN); + + try { + start(managementControllerKey, discoveryKey); + } catch (IOException e) { + throw new IllegalStateException("Unable to start networking thread", e); + } + } + + @Override + public void dispose() { + if (networkingThread.isAlive()) { + networkingThread.interrupt(); + try { + networkingThread.join(TimeUnit.SECONDS.toMillis(5)); + } catch (InterruptedException e) { + logger.debug("Interrupted while closing", e); + } + } + + @Nullable + final EchonetChannel echonetChannel = this.echonetChannel; + if (null != echonetChannel) { + echonetChannel.close(); + } + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + } + + @Override + public Collection> getServices() { + return Collections.singletonList(EchonetDiscoveryService.class); + } + + private abstract static class Message { + final InstanceKey instanceKey; + + public Message(InstanceKey instanceKey) { + this.instanceKey = instanceKey; + } + } + + private static final class NewDeviceMessage extends Message { + final long pollIntervalMs; + final long retryTimeoutMs; + final EchonetDeviceListener echonetDeviceListener; + + public NewDeviceMessage(final InstanceKey instanceKey, long pollIntervalMs, long retryTimeoutMs, + final EchonetDeviceListener echonetDeviceListener) { + super(instanceKey); + this.pollIntervalMs = pollIntervalMs; + this.retryTimeoutMs = retryTimeoutMs; + this.echonetDeviceListener = echonetDeviceListener; + } + + @Override + public String toString() { + return "NewDeviceMessage{" + "instanceKey=" + instanceKey + ", pollIntervalMs=" + pollIntervalMs + + ", retryTimeoutMs=" + retryTimeoutMs + "} " + super.toString(); + } + } + + private static class RefreshMessage extends Message { + private final String channelId; + + public RefreshMessage(InstanceKey instanceKey, String channelId) { + super(instanceKey); + this.channelId = channelId; + } + } + + private static class RemoveDevice extends Message { + public RemoveDevice(final InstanceKey instanceKey) { + super(instanceKey); + } + } + + private static class StartDiscoveryMessage extends Message { + private final EchonetDiscoveryListener echonetDiscoveryListener; + + public StartDiscoveryMessage(EchonetDiscoveryListener echonetDiscoveryListener, InstanceKey discoveryKey) { + super(discoveryKey); + this.echonetDiscoveryListener = echonetDiscoveryListener; + } + } + + private static class StopDiscoveryMessage extends Message { + public StopDiscoveryMessage(InstanceKey discoveryKey) { + super(discoveryKey); + } + } + + private static class UpdateDevice extends Message { + private final String channelId; + private final State state; + + public UpdateDevice(final InstanceKey instanceKey, final String channelId, final State state) { + super(instanceKey); + this.channelId = channelId; + this.state = state; + } + + public String toString() { + return "UpdateDevice{" + "instanceKey=" + instanceKey + ", channelId='" + channelId + '\'' + ", state=" + + state + "} " + super.toString(); + } + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetLiteHandler.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetLiteHandler.java new file mode 100644 index 00000000000..87297e04da5 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetLiteHandler.java @@ -0,0 +1,188 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import static java.util.Objects.requireNonNull; +import static org.openhab.binding.echonetlite.internal.EchonetLiteBindingConstants.PROPERTY_NAME_INSTANCE_KEY; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.thing.Bridge; +import org.openhab.core.thing.Channel; +import org.openhab.core.thing.ChannelUID; +import org.openhab.core.thing.Thing; +import org.openhab.core.thing.ThingStatus; +import org.openhab.core.thing.ThingStatusDetail; +import org.openhab.core.thing.binding.BaseThingHandler; +import org.openhab.core.thing.binding.builder.ChannelBuilder; +import org.openhab.core.thing.binding.builder.ThingBuilder; +import org.openhab.core.thing.type.ChannelTypeUID; +import org.openhab.core.types.Command; +import org.openhab.core.types.RefreshType; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * The {@link EchonetLiteHandler} is responsible for handling commands, which are + * sent to one of the channels. + * + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public class EchonetLiteHandler extends BaseThingHandler implements EchonetDeviceListener { + private final Logger logger = LoggerFactory.getLogger(EchonetLiteHandler.class); + + private @Nullable InstanceKey instanceKey; + private final Map stateByChannelId = new HashMap<>(); + + public EchonetLiteHandler(final Thing thing) { + super(thing); + } + + @Nullable + private EchonetLiteBridgeHandler bridgeHandler() { + @Nullable + final Bridge bridge = getBridge(); + if (null == bridge) { + return null; + } + + @Nullable + final EchonetLiteBridgeHandler handler = (EchonetLiteBridgeHandler) bridge.getHandler(); + return handler; + } + + @Override + public void handleCommand(ChannelUID channelUID, Command command) { + @Nullable + final EchonetLiteBridgeHandler handler = bridgeHandler(); + if (null == handler) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.conf-error.null-bridge-handler"); + return; + } + + if (command instanceof RefreshType) { + logger.debug("Refreshing: {}", channelUID); + + final State currentState = stateByChannelId.get(channelUID.getId()); + if (null == currentState) { + handler.refreshDevice(requireNonNull(instanceKey), channelUID.getId()); + } else { + updateState(channelUID, currentState); + } + } else if (command instanceof State) { + logger.debug("Updating: {} to {}", channelUID, command); + + handler.updateDevice(requireNonNull(instanceKey), channelUID.getId(), (State) command); + } + } + + @Override + public void initialize() { + final EchonetDeviceConfig config = getConfigAs(EchonetDeviceConfig.class); + + logger.debug("Initialising: {}", config); + + updateStatus(ThingStatus.UNKNOWN); + + @Nullable + final EchonetLiteBridgeHandler bridgeHandler = bridgeHandler(); + if (null == bridgeHandler) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.conf-error.null-bridge-handler"); + return; + } + + try { + final InetSocketAddress address = new InetSocketAddress(requireNonNull(config.hostname), config.port); + final InstanceKey instanceKey = new InstanceKey(address, + EchonetClass.resolve(config.groupCode, config.classCode), config.instance); + this.instanceKey = instanceKey; + + updateProperty(PROPERTY_NAME_INSTANCE_KEY, instanceKey.representationProperty()); + bridgeHandler.newDevice(instanceKey, config.pollIntervalMs, config.retryTimeoutMs, this); + } catch (Exception e) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, e.getMessage()); + } + } + + public void handleRemoval() { + @Nullable + final EchonetLiteBridgeHandler bridgeHandler = bridgeHandler(); + if (null == bridgeHandler) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, + "@text/offline.conf-error.null-bridge-handler"); + return; + } + + bridgeHandler.removeDevice(requireNonNull(instanceKey)); + } + + public void onInitialised(String identifier, InstanceKey instanceKey, Map channelIdAndType) { + logger.debug("Initialised Channels: {}", channelIdAndType); + + final List toAddChannelFor = new ArrayList<>(); + + for (String channelId : channelIdAndType.keySet()) { + if (null == thing.getChannel(channelId)) { + toAddChannelFor.add(channelId); + } + } + + logger.debug("Adding Channels: {}", toAddChannelFor); + + if (!toAddChannelFor.isEmpty()) { + final ThingBuilder thingBuilder = editThing(); + + for (String channelId : toAddChannelFor) { + final Channel channel = ChannelBuilder.create(new ChannelUID(thing.getUID(), channelId)) + .withAcceptedItemType(channelIdAndType.get(channelId)) + .withType(new ChannelTypeUID(thing.getThingTypeUID().getBindingId(), channelId)).build(); + thingBuilder.withChannel(channel); + + logger.debug("Added Channel: {}", channel); + } + + updateThing(thingBuilder.build()); + } + + updateStatus(ThingStatus.ONLINE); + } + + public void onUpdated(final String channelId, final State value) { + stateByChannelId.put(channelId, value); + + if (ThingStatus.ONLINE != getThing().getStatus()) { + updateStatus(ThingStatus.ONLINE); + } + updateState(channelId, value); + } + + public void onRemoved() { + updateStatus(ThingStatus.REMOVED); + } + + public void onOffline() { + if (ThingStatus.OFFLINE != getThing().getStatus()) { + updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR); + } + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetLiteHandlerFactory.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetLiteHandlerFactory.java new file mode 100644 index 00000000000..158dd29b7d6 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetLiteHandlerFactory.java @@ -0,0 +1,60 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import static org.openhab.binding.echonetlite.internal.EchonetLiteBindingConstants.THING_TYPE_ECHONET_BRIDGE; +import static org.openhab.binding.echonetlite.internal.EchonetLiteBindingConstants.THING_TYPE_ECHONET_DEVICE; + +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +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 EchonetLiteHandlerFactory} is responsible for creating things and thing + * handlers. + * + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +@Component(configurationPid = "binding.echonetlite", service = ThingHandlerFactory.class) +public class EchonetLiteHandlerFactory extends BaseThingHandlerFactory { + + private static final Set SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_ECHONET_DEVICE, + THING_TYPE_ECHONET_BRIDGE); + + @Override + public boolean supportsThingType(ThingTypeUID thingTypeUID) { + return SUPPORTED_THING_TYPES_UIDS.contains(thingTypeUID); + } + + @Override + protected @Nullable ThingHandler createHandler(Thing thing) { + final ThingTypeUID thingTypeUID = thing.getThingTypeUID(); + + if (THING_TYPE_ECHONET_DEVICE.equals(thingTypeUID)) { + return new EchonetLiteHandler(thing); + } else if (THING_TYPE_ECHONET_BRIDGE.equals(thingTypeUID)) { + return new EchonetLiteBridgeHandler((Bridge) thing); + } + + return null; + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetMessage.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetMessage.java new file mode 100644 index 00000000000..061fb896bf3 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetMessage.java @@ -0,0 +1,134 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public class EchonetMessage { + public static final int TID_OFFSET = 2; + public static final int GROUP_OFFSET = 4; + public static final int CLASS_OFFSET = 5; + public static final int INSTANCE_OFFSET = 6; + public static final int ESV_OFFSET = 10; + public static final int OPC_OFFSET = 11; + public static final int PROPERTY_OFFSET = 12; + + private final ByteBuffer messageData = ByteBuffer.allocateDirect(65536); + private final ByteBuffer propertyData = messageData.duplicate(); + private int propertyCursor = 0; + private int currentProperty = -1; + + @Nullable + private SocketAddress address; + + public ByteBuffer bufferForRead() { + reset(); + return messageData; + } + + private void reset() { + messageData.clear(); + messageData.order(ByteOrder.BIG_ENDIAN); + propertyCursor = 0; + currentProperty = -1; + } + + public void sourceAddress(final SocketAddress address) { + this.address = address; + } + + public @Nullable SocketAddress sourceAddress() { + return address; + } + + public @Nullable EchonetClass sourceClass() { + return EchonetClassIndex.INSTANCE.lookup(messageData.get(GROUP_OFFSET), messageData.get(CLASS_OFFSET)); + } + + public byte instance() { + return messageData.get(INSTANCE_OFFSET); + } + + public Esv esv() { + return Esv.forCode(messageData.get(ESV_OFFSET)); + } + + public int numProperties() { + return 0xFF & messageData.get(OPC_OFFSET); + } + + public boolean moveNext() { + if (propertyCursor < numProperties()) { + propertyCursor++; + if (-1 == currentProperty) { + currentProperty = PROPERTY_OFFSET; + } else { + int pdc = 0xFF & messageData.get(currentProperty + 1); + currentProperty = currentProperty + 2 + pdc; + } + return true; + } + + return false; + } + + public int currentEpc() { + return messageData.get(currentProperty) & 0xFF; + } + + public int currentPdc() { + return messageData.get(currentProperty + 1) & 0xFF; + } + + public ByteBuffer currentEdt() { + propertyData.clear(); + propertyData.position(currentProperty + 2).limit(currentProperty + 2 + currentPdc()); + return propertyData; + } + + public short tid() { + return messageData.getShort(TID_OFFSET); + } + + public String toDebug() { + return "EchonetMessage{" + "sourceAddress=" + sourceAddress() + ", class=" + sourceClass() + ", instance=" + + instance() + ", num properties=" + numProperties() + ", data=" + dumpData() + '}'; + } + + private String dumpData() { + final byte[] bs = new byte[messageData.limit()]; + final ByteBuffer duplicate = messageData.duplicate(); + duplicate.position(0).limit(messageData.limit()); + duplicate.get(bs); + + final StringBuilder sb = new StringBuilder(); + + sb.append('['); + for (byte b : bs) { + sb.append("0x").append(Integer.toHexString(0xFF & b)).append(", "); + } + sb.setLength(sb.length() - 2); + sb.append(']'); + + return sb.toString(); + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetMessageBuilder.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetMessageBuilder.java new file mode 100644 index 00000000000..2282eca29cb --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetMessageBuilder.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import static org.openhab.binding.echonetlite.internal.LangUtil.b; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.ByteOrder; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public class EchonetMessageBuilder { + private static final byte EHD_1 = 0x10; + private static final byte EHD_2 = (byte) (0x81 & 0xFF); + + private final ByteBuffer buffer; + private final ByteBuffer edtBuffer = ByteBuffer.allocate(4096); + private int opcPosition = 0; + @Nullable + private InetSocketAddress destAddress; + + public EchonetMessageBuilder() { + buffer = ByteBuffer.allocateDirect(4096).order(ByteOrder.BIG_ENDIAN); + } + + public void start(short tid, InstanceKey source, InstanceKey dest, Esv service) { + // 1081000005ff010ef0006201d60100 + // 1081000105ff010ef0006201d600 + // 0000 10 81 00 00 05 ff 01 0e f0 00 62 01 d6 01 00 + // 0000 10 81 00 01 05 ff 01 0e f0 00 62 01 d6 00 + + destAddress = dest.address; + + buffer.clear(); + buffer.put(EHD_1); + buffer.put(EHD_2); + buffer.putShort(tid); + buffer.put(b(source.klass.groupCode())); + buffer.put(b(source.klass.classCode())); + buffer.put(b(source.instance)); + buffer.put(b(dest.klass.groupCode())); + buffer.put(b(dest.klass.classCode())); + buffer.put(b(dest.instance)); + buffer.put(service.code()); + + opcPosition = buffer.position(); + buffer.put((byte) 0); + } + + private void incrementOpc() { + buffer.put(opcPosition, (byte) (buffer.get(opcPosition) + 1)); + } + + public void append(final byte edt, final byte length, final byte value) { + buffer.put(edt).put(length).put(value); + incrementOpc(); + } + + public void appendEpcRequest(final int epc) { + buffer.put(b(epc)).put((byte) 0); + incrementOpc(); + } + + public ByteBuffer buffer() { + return buffer; + } + + @Nullable + public SocketAddress address() { + return destAddress; + } + + public ByteBuffer edtBuffer() { + edtBuffer.clear(); + return edtBuffer; + } + + public void appendEpcUpdate(final int epc, ByteBuffer edtBuffer) { + if (edtBuffer.remaining() < 0 || 255 < edtBuffer.remaining()) { + throw new IllegalArgumentException("Invalid update value, length: " + edtBuffer.remaining()); + } + + buffer.put(b(epc)).put(b(edtBuffer.remaining())).put(edtBuffer); + incrementOpc(); + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetObject.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetObject.java new file mode 100644 index 00000000000..28d918e742e --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetObject.java @@ -0,0 +1,200 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import static org.openhab.binding.echonetlite.internal.EchonetLiteBindingConstants.DEFAULT_RETRY_TIMEOUT_MS; + +import java.nio.ByteBuffer; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public abstract class EchonetObject { + + private final Logger logger = LoggerFactory.getLogger(EchonetObject.class); + + protected final InstanceKey instanceKey; + protected final HashSet pendingGets = new HashSet<>(); + + protected InflightRequest inflightGetRequest = new InflightRequest(DEFAULT_RETRY_TIMEOUT_MS, "GET"); + protected InflightRequest inflightSetRequest = new InflightRequest(DEFAULT_RETRY_TIMEOUT_MS, "SET"); + + protected long pollIntervalMs; + + public EchonetObject(final InstanceKey instanceKey, final Epc initialProperty) { + this.instanceKey = instanceKey; + pendingGets.add(initialProperty); + } + + public InstanceKey instanceKey() { + return instanceKey; + } + + public void applyProperty(InstanceKey sourceInstanceKey, Esv esv, final int epcCode, final int pdc, + final ByteBuffer edt) { + } + + public boolean buildPollMessage(final EchonetMessageBuilder messageBuilder, final ShortSupplier tidSupplier, + long nowMs, InstanceKey managementControllerKey) { + if (pendingGets.isEmpty()) { + return false; + } + + if (hasInflight(nowMs, this.inflightGetRequest)) { + return false; + } + + final short tid = tidSupplier.getAsShort(); + messageBuilder.start(tid, managementControllerKey, instanceKey(), Esv.Get); + + for (Epc pendingProperty : pendingGets) { + messageBuilder.appendEpcRequest(pendingProperty.code()); + } + + this.inflightGetRequest.requestSent(tid, nowMs); + + return true; + } + + protected boolean hasInflight(long nowMs, InflightRequest inflightRequest) { + if (inflightRequest.isInflight()) { + return !inflightRequest.hasTimedOut(nowMs); + } + return false; + } + + protected void setTimeouts(long pollIntervalMs, long retryTimeoutMs) { + this.pollIntervalMs = pollIntervalMs; + this.inflightGetRequest = new InflightRequest(retryTimeoutMs, inflightGetRequest); + this.inflightSetRequest = new InflightRequest(retryTimeoutMs, inflightSetRequest); + } + + public boolean buildUpdateMessage(final EchonetMessageBuilder messageBuilder, final ShortSupplier tid, + final long nowMs, InstanceKey managementControllerKey) { + return false; + } + + public void refreshAll(long nowMs) { + } + + public String toString() { + return "ItemBase{" + "instanceKey=" + instanceKey + ", pendingProperties=" + pendingGets + '}'; + } + + public void update(String channelId, State state) { + } + + public void removed() { + } + + public void refresh(String channelId) { + } + + public void applyHeader(Esv esv, short tid, long nowMs) { + if ((esv == Esv.Get_Res || esv == Esv.Get_SNA)) { + final long sentTimestampMs = this.inflightGetRequest.timestampMs; + if (this.inflightGetRequest.responseReceived(tid)) { + logger.debug("{} response time: {}ms", esv, nowMs - sentTimestampMs); + } else { + logger.warn("Unexpected {} response: {}", esv, tid); + this.inflightGetRequest.checkOldResponse(tid, nowMs); + } + } else if ((esv == Esv.Set_Res || esv == Esv.SetC_SNA)) { + final long sentTimestampMs = this.inflightSetRequest.timestampMs; + if (this.inflightSetRequest.responseReceived(tid)) { + logger.debug("{} response time: {}ms", esv, nowMs - sentTimestampMs); + } else { + logger.warn("Unexpected {} response: {}", esv, tid); + this.inflightSetRequest.checkOldResponse(tid, nowMs); + } + } + } + + public void checkTimeouts() { + } + + protected static class InflightRequest { + private static final long NULL_TIMESTAMP = -1; + + private final Logger logger = LoggerFactory.getLogger(InflightRequest.class); + private final long timeoutMs; + private final String name; + private final Map oldRequests = new HashMap<>(); + + private short tid; + private long timestampMs = NULL_TIMESTAMP; + @SuppressWarnings("unused") + private int timeoutCount = 0; + + InflightRequest(long timeoutMs, InflightRequest existing) { + this(timeoutMs, existing.name); + this.tid = existing.tid; + this.timestampMs = existing.timestampMs; + } + + InflightRequest(long timeoutMs, String name) { + this.timeoutMs = timeoutMs; + this.name = name; + } + + void requestSent(short tid, long timestampMs) { + this.tid = tid; + this.timestampMs = timestampMs; + } + + boolean responseReceived(short tid) { + timestampMs = NULL_TIMESTAMP; + timeoutCount = 0; + + return this.tid == tid; + } + + boolean hasTimedOut(long nowMs) { + final boolean timedOut = timestampMs + timeoutMs <= nowMs; + if (timedOut) { + logger.debug("Timed out {}, tid={}, timestampMs={} + timeoutMs={} <= nowMs={}", name, tid, timestampMs, + timeoutMs, nowMs); + timeoutCount++; + + if (NULL_TIMESTAMP != tid) { + oldRequests.put(tid, timestampMs); + } + } + return timedOut; + } + + public boolean isInflight() { + return NULL_TIMESTAMP != timestampMs; + } + + public void checkOldResponse(short tid, long nowMs) { + final Long oldResponseTimestampMs = oldRequests.remove(tid); + if (null != oldResponseTimestampMs) { + logger.debug("Timed out request, tid={}, actually took={}", tid, nowMs - oldResponseTimestampMs); + } + } + + public int timeoutCount() { + return timeoutCount; + } + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetProfileNode.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetProfileNode.java new file mode 100644 index 00000000000..af4046acc9f --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetProfileNode.java @@ -0,0 +1,82 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import static org.openhab.binding.echonetlite.internal.EchonetLiteBindingConstants.DEFAULT_POLL_INTERVAL_MS; +import static org.openhab.binding.echonetlite.internal.EchonetLiteBindingConstants.DEFAULT_RETRY_TIMEOUT_MS; + +import java.nio.ByteBuffer; +import java.util.Map; +import java.util.function.Consumer; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public class EchonetProfileNode extends EchonetObject implements EchonetDeviceListener { + + private final Consumer newDeviceListener; + private final EchonetDiscoveryListener echonetDiscoveryListener; + private long lastPollMs = 0; + + public EchonetProfileNode(final InstanceKey instanceKey, Consumer newDeviceListener, + EchonetDiscoveryListener echonetDiscoveryListener) { + super(instanceKey, Epc.NodeProfile.SELF_NODE_INSTANCE_LIST_S); + this.newDeviceListener = newDeviceListener; + this.echonetDiscoveryListener = echonetDiscoveryListener; + setTimeouts(DEFAULT_POLL_INTERVAL_MS, DEFAULT_RETRY_TIMEOUT_MS); + } + + @Override + public void applyProperty(InstanceKey sourceInstanceKey, Esv esv, int epcCode, int pdc, ByteBuffer edt) { + final Epc epc = Epc.lookup(instanceKey().klass.groupCode(), instanceKey().klass.classCode(), epcCode); + + if (EchonetClass.NODE_PROFILE == sourceInstanceKey.klass && Epc.NodeProfile.SELF_NODE_INSTANCE_LIST_S == epc) { + final int selfNodeInstanceCount = edt.get() & 0xFF; + + for (int i = 0; i < selfNodeInstanceCount && edt.hasRemaining(); i++) { + final byte groupCode = edt.get(); + final byte classCode = edt.get(); + final byte instance = edt.get(); + final EchonetClass itemClass = EchonetClassIndex.INSTANCE.lookup(groupCode, classCode); + + final InstanceKey newItemKey = new InstanceKey(sourceInstanceKey.address, itemClass, instance); + final EchonetDevice discoveredDevice = new EchonetDevice(newItemKey, this); + discoveredDevice.setTimeouts(DEFAULT_POLL_INTERVAL_MS, DEFAULT_RETRY_TIMEOUT_MS); + newDeviceListener.accept(discoveredDevice); + } + } + } + + @Override + public boolean buildPollMessage(EchonetMessageBuilder messageBuilder, ShortSupplier tidSupplier, long nowMs, + InstanceKey managementControllerKey) { + boolean result = false; + if (lastPollMs + pollIntervalMs <= nowMs) { + result = super.buildPollMessage(messageBuilder, tidSupplier, nowMs, managementControllerKey); + + if (result) { + lastPollMs = nowMs; + } + } + + return result; + } + + @Override + public void onInitialised(String identifier, InstanceKey instanceKey, Map channelIdAndType) { + echonetDiscoveryListener.onDeviceFound(identifier, instanceKey); + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetPropertyMap.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetPropertyMap.java new file mode 100644 index 00000000000..40d1caf6162 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EchonetPropertyMap.java @@ -0,0 +1,92 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import java.nio.ByteBuffer; +import java.util.Collection; +import java.util.Set; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public class EchonetPropertyMap { + private static final int[][] PROPERTY_MAP = { { 0x80, 0x90, 0xA0, 0xB0, 0xC0, 0xD0, 0xE0, 0xF0, }, + { 0x81, 0x91, 0xA1, 0xB1, 0xC1, 0xD1, 0xE1, 0xF1, }, { 0x82, 0x92, 0xA2, 0xB2, 0xC2, 0xD2, 0xE2, 0xF2, }, + { 0x83, 0x93, 0xA3, 0xB3, 0xC3, 0xD3, 0xE3, 0xF3, }, { 0x84, 0x94, 0xA4, 0xB4, 0xC4, 0xD4, 0xE4, 0xF4, }, + { 0x85, 0x95, 0xA5, 0xB5, 0xC5, 0xD5, 0xE5, 0xF5, }, { 0x86, 0x96, 0xA6, 0xB6, 0xC6, 0xD6, 0xE6, 0xF6, }, + { 0x87, 0x97, 0xA7, 0xB7, 0xC7, 0xD7, 0xE7, 0xF7, }, { 0x88, 0x98, 0xA8, 0xB8, 0xC8, 0xD8, 0xE8, 0xF8, }, + { 0x89, 0x99, 0xA9, 0xB9, 0xC9, 0xD9, 0xE9, 0xF9, }, { 0x8A, 0x9A, 0xAA, 0xBA, 0xCA, 0xDA, 0xEA, 0xFA, }, + { 0x8B, 0x9B, 0xAB, 0xBB, 0xCB, 0xDB, 0xEB, 0xFB, }, { 0x8C, 0x9C, 0xAC, 0xBC, 0xCC, 0xDC, 0xEC, 0xFC, }, + { 0x8D, 0x9D, 0xAD, 0xBD, 0xCD, 0xDD, 0xED, 0xFD, }, { 0x8E, 0x9E, 0xAE, 0xBE, 0xCE, 0xDE, 0xEE, 0xFE, }, + { 0x8F, 0x9F, 0xAF, 0xBF, 0xCF, 0xDF, 0xEF, 0xFF, }, }; + + private int[] propertyMap = {}; + private final Epc epc; + + public EchonetPropertyMap(final Epc epc) { + this.epc = epc; + } + + public Epc epc() { + return epc; + } + + public void update(final ByteBuffer edt) { + propertyMap = parsePropertyMap(edt); + } + + public void getProperties(int groupCode, int classCode, final Set existing, Collection toFill) { + for (int epcCode : propertyMap) { + final Epc epc = Epc.lookup(groupCode, classCode, epcCode); + if (!existing.contains(epc)) { + toFill.add(epc); + } + } + } + + static int[] parsePropertyMap(final ByteBuffer buffer) { + final int numProperties = buffer.get() & 0xFF; + final int[] properties = new int[numProperties]; + int propertyIndex = 0; + if (numProperties < 16) { + for (int i = 0; i < numProperties; i++) { + properties[propertyIndex] = (buffer.get() & 0xFF); + propertyIndex++; + } + } else { + assert 16 == buffer.remaining(); + + for (int i = 0; i < 16; i++) { + int b = buffer.get() & 0xFF; + for (int j = 0; j < 8; j++) { + if (0 != (b & (1 << j))) { + assert propertyIndex < properties.length; + + properties[propertyIndex] = PROPERTY_MAP[i][j]; + propertyIndex++; + } + } + } + } + + assert propertyIndex == properties.length; + return properties; + } + + public String toString() { + return "EnPropertyMap{" + "propertyMap=" + HexUtil.hex(propertyMap) + '}'; + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/Epc.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/Epc.java new file mode 100644 index 00000000000..b09b82364e4 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/Epc.java @@ -0,0 +1,487 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import static org.openhab.binding.echonetlite.internal.EchonetLiteBindingConstants.ON_OFF_CODEC_30_31; +import static org.openhab.binding.echonetlite.internal.EchonetLiteBindingConstants.ON_OFF_CODEC_41_42; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.binding.echonetlite.internal.StateCodec.Option; +import org.openhab.binding.echonetlite.internal.StateCodec.OptionCodec; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public interface Epc { + int code(); + + String name(); + + @Nullable + default String type() { + return null; + } + + default String channelId() { + return LangUtil.constantToVariable(name()); + } + + @Nullable + default StateDecode decoder() { + return null; + } + + @Nullable + default StateEncode encoder() { + return null; + } + + static Epc lookup(int groupCode, int classCode, int epcCode) { + return EpcLookupTable.INSTANCE.resolve(groupCode, classCode, epcCode); + } + + // ECHONET SPECIFICATION + // APPENDIX Detailed Requirements for ECHONET Device objects + // Table 2-1 + enum Device implements Epc { + // @formatter:off + OPERATION_STATUS(0x80, ON_OFF_CODEC_30_31), + + INSTALLATION_LOCATION(0x81, new OptionCodec( + new Option("Not specified", 0b00000_000), + + new Option("Living Room", 0b00001_000), + new Option("Living Room 1", 0b00001_001), + new Option("Living Room 2", 0b00001_010), + new Option("Living Room 3", 0b00001_011), + new Option("Living Room 4", 0b00001_100), + new Option("Living Room 5", 0b00001_101), + new Option("Living Room 6", 0b00001_110), + new Option("Living Room 7", 0b00001_111), + + new Option("Dining Room", 0b00010_000), + new Option("Dining Room 1", 0b00010_001), + new Option("Dining Room 2", 0b00010_010), + new Option("Dining Room 3", 0b00010_011), + new Option("Dining Room 4", 0b00010_100), + new Option("Dining Room 5", 0b00010_101), + new Option("Dining Room 6", 0b00010_110), + new Option("Dining Room 7", 0b00010_111), + + new Option("Kitchen", 0b00011_000), + new Option("Kitchen 1", 0b00011_001), + new Option("Kitchen 2", 0b00011_010), + new Option("Kitchen 3", 0b00011_011), + new Option("Kitchen 4", 0b00011_100), + new Option("Kitchen 5", 0b00011_101), + new Option("Kitchen 6", 0b00011_110), + new Option("Kitchen 7", 0b00011_111), + + new Option("Lavatory", 0b00100_000), + new Option("Lavatory 1", 0b00100_001), + new Option("Lavatory 2", 0b00100_010), + new Option("Lavatory 3", 0b00100_011), + new Option("Lavatory 4", 0b00100_100), + new Option("Lavatory 5", 0b00100_101), + new Option("Lavatory 6", 0b00100_110), + new Option("Lavatory 7", 0b00100_111), + + new Option("Washroom/changing room", 0b00101_000), + new Option("Washroom/changing room 1", 0b00101_001), + new Option("Washroom/changing room 2", 0b00101_010), + new Option("Washroom/changing room 3", 0b00101_011), + new Option("Washroom/changing room 4", 0b00101_100), + new Option("Washroom/changing room 5", 0b00101_101), + new Option("Washroom/changing room 6", 0b00101_110), + new Option("Washroom/changing room 7", 0b00101_111), + + new Option("Passageway", 0b00111_000), + new Option("Passageway 1", 0b00111_001), + new Option("Passageway 2", 0b00111_010), + new Option("Passageway 3", 0b00111_011), + new Option("Passageway 4", 0b00111_100), + new Option("Passageway 5", 0b00111_101), + new Option("Passageway 6", 0b00111_110), + new Option("Passageway 7", 0b00111_111), + + new Option("Room", 0b01000_000), + new Option("Room 1", 0b01000_001), + new Option("Room 2", 0b01000_010), + new Option("Room 3", 0b01000_011), + new Option("Room 4", 0b01000_100), + new Option("Room 5", 0b01000_101), + new Option("Room 6", 0b01000_110), + new Option("Room 7", 0b01000_111), + + new Option("Stairway", 0b01001_000), + new Option("Stairway 1", 0b01001_001), + new Option("Stairway 2", 0b01001_010), + new Option("Stairway 3", 0b01001_011), + new Option("Stairway 4", 0b01001_100), + new Option("Stairway 5", 0b01001_101), + new Option("Stairway 6", 0b01001_110), + new Option("Stairway 7", 0b01001_111), + + new Option("Front door", 0b01010_000), + new Option("Front door 1", 0b01010_001), + new Option("Front door 2", 0b01010_010), + new Option("Front door 3", 0b01010_011), + new Option("Front door 4", 0b01010_100), + new Option("Front door 5", 0b01010_101), + new Option("Front door 6", 0b01010_110), + new Option("Front door 7", 0b01010_111), + + new Option("Storeroom", 0b01011_000), + new Option("Storeroom 1", 0b01011_001), + new Option("Storeroom 2", 0b01011_010), + new Option("Storeroom 3", 0b01011_011), + new Option("Storeroom 4", 0b01011_100), + new Option("Storeroom 5", 0b01011_101), + new Option("Storeroom 6", 0b01011_110), + new Option("Storeroom 7", 0b01011_111), + + new Option("Garden/perimeter", 0b01100_000), + new Option("Garden/perimeter 1", 0b01100_001), + new Option("Garden/perimeter 2", 0b01100_010), + new Option("Garden/perimeter 3", 0b01100_011), + new Option("Garden/perimeter 4", 0b01100_100), + new Option("Garden/perimeter 5", 0b01100_101), + new Option("Garden/perimeter 6", 0b01100_110), + new Option("Garden/perimeter 7", 0b01100_111), + + new Option("Garage", 0b01101_000), + new Option("Garage 1", 0b01101_001), + new Option("Garage 2", 0b01101_010), + new Option("Garage 3", 0b01101_011), + new Option("Garage 4", 0b01101_100), + new Option("Garage 5", 0b01101_101), + new Option("Garage 6", 0b01101_110), + new Option("Garage 7", 0b01101_111), + + new Option("Veranda/balcony", 0b01110_000), + new Option("Veranda/balcony 1", 0b01110_001), + new Option("Veranda/balcony 2", 0b01110_010), + new Option("Veranda/balcony 3", 0b01110_011), + new Option("Veranda/balcony 4", 0b01110_100), + new Option("Veranda/balcony 5", 0b01110_101), + new Option("Veranda/balcony 6", 0b01110_110), + new Option("Veranda/balcony 7", 0b01110_111), + + new Option("Others", 0b01111_000), + new Option("Others 1", 0b01111_001), + new Option("Others 2", 0b01111_010), + new Option("Others 3", 0b01111_011), + new Option("Others 4", 0b01111_100), + new Option("Others 5", 0b01111_101), + new Option("Others 6", 0b01111_110), + new Option("Others 7", 0b01111_111))), + + STANDARD_VERSION_INFORMATION(0x82, StateCodec.StandardVersionInformationCodec.INSTANCE, null), + IDENTIFICATION_NUMBER(0x83, StateCodec.HexStringCodec.INSTANCE, null), + MEASURED_INSTANTANEOUS_POWER_CONSUMPTION(0x84), + MEASURED_CUMULATIVE_POWER_CONSUMPTION(0x85), + MANUFACTURER_FAULT_CODE(0x86, StateCodec.HexStringCodec.INSTANCE, null), + CURRENT_LIMIT_SETTING(0x87), + FAULT_STATUS(0x88, ON_OFF_CODEC_41_42, null), + FAULT_DESCRIPTION(0x89, StateCodec.HexStringCodec.INSTANCE, null), + MANUFACTURER_CODE(0x8A, StateCodec.HexStringCodec.INSTANCE, null), + BUSINESS_FACILITY_CODE(0x8B, StateCodec.HexStringCodec.INSTANCE, null), + PRODUCT_CODE(0x8C), + PRODUCTION_NUMBER(0x8D), + PRODUCTION_DATE(0x8E), + POWER_SAVING_OPERATION_SETTING(0x8F, ON_OFF_CODEC_41_42), + REMOTE_CONTROL_SETTING(0x93), + CURRENT_TIME_SETTING(0x97), + CURRENT_DATE_SETTING(0x98), + POWER_LIMIT_SETTING(0x99), + CUMULATIVE_OPERATING_TIME(0x9A, StateCodec.OperatingTimeDecode.INSTANCE, null), + SETM_PROPERTY_MAP(0x9B), + GETM_PROPERTY_MAP(0x9C), + STATUS_CHANGE_ANNOUNCEMENT_PROPERTY_MAP(0x9D), + SET_PROPERTY_MAP(0x9E), + GET_PROPERTY_MAP(0x9F); + // @formatter:on + + public final int code; + @Nullable + public final StateDecode stateDecode; + @Nullable + public final StateEncode stateEncode; + + Device(int code) { + this(code, null, null); + } + + Device(int code, @Nullable StateDecode stateDecode, @Nullable StateEncode stateEncode) { + this.code = code; + this.stateDecode = stateDecode; + this.stateEncode = stateEncode; + } + + Device(int code, StateCodec stateCodec) { + this(code, stateCodec, stateCodec); + } + + public int code() { + return code; + } + + @Nullable + public StateDecode decoder() { + return stateDecode; + } + + @Nullable + public StateEncode encoder() { + return stateEncode; + } + } + + enum AcGroup implements Epc { + // @formatter:off + AIR_FLOW_RATE(0xA0, new OptionCodec( + new Option("Auto", 0x41), + new Option("Rate 1", 0x31), + new Option("Rate 2", 0x32), + new Option("Rate 3", 0x33), + new Option("Rate 4", 0x34), + new Option("Rate 5", 0x35), + new Option("Rate 6", 0x36), + new Option("Rate 7", 0x37), + new Option("Rate 8", 0x38))), + + AUTOMATIC_CONTROL_OF_AIR_FLOW_DIRECTION(0xA1, new OptionCodec( + new Option("Automatic", 0x41), + new Option("Non-automatic", 0x42), + new Option("Automatic (vertical)", 0x43), + new Option("Automatic (horizontal)", 0x44))), + + AUTOMATIC_SWING_OF_AIR_FLOW(0xA3, new OptionCodec( + new Option("Not used", 0x31), + new Option("Used (vertical)", 0x41), + new Option("Used (horizontal)", 0x42), + new Option("Used (vertical and horizontal)", 0x43))), + + AIR_FLOW_DIRECTION_VERTICAL(0xA4, new OptionCodec( + new Option("Uppermost", 0x41), + new Option("Lowermost", 0x42), + new Option("Mid-uppermost", 0x43), + new Option("Mid-lowermost", 0x44), + new Option("Central", 0x45))), + + AIR_FLOW_DIRECTION_HORIZONTAL(0xA5, new OptionCodec( + new Option("XXXOO", 0x41), + new Option("OOXXX", 0x42), + new Option("XOOOX", 0x43), + new Option("OOXOO", 0x44), + new Option("XXXXO", 0x51), + new Option("XXXOX", 0x52), + new Option("XXOXX", 0x54), + new Option("XXOXO", 0x55), + new Option("XXOOX", 0x56), + new Option("XXOOO", 0x57), + new Option("XOXXX", 0x58), + new Option("XOXXO", 0x59), + new Option("XOXOX", 0x5A), + new Option("XOXOO", 0x5B), + new Option("XOOXX", 0x5C), + new Option("XOOXO", 0x5D), + new Option("XOOOO", 0x5F), + new Option("OXXXX", 0x60), + new Option("OXXXO", 0x61), + new Option("OXXOX", 0x62), + new Option("OXXOO", 0x63), + new Option("OXOXX", 0x64), + new Option("OXOXO", 0x65), + new Option("OXOOX", 0x66), + new Option("OXOOO", 0x67), + new Option("OOXXO", 0x69), + new Option("OOXOX", 0x6A), + new Option("OOOXX", 0x6C), + new Option("OOOXO", 0x6D), + new Option("OOOOX", 0x6E), + new Option("OOOOO", 0x6F))), + + SPECIAL_STATE(0xAA), + NON_PRIORITY_STATE(0xAB), + OPERATION_MODE(0xB0, new OptionCodec( + new Option("Automatic", 0x41), + new Option("Cooling", 0x42), + new Option("Heating", 0x43), + new Option("Dry", 0x44), + new Option("Fan", 0x45), + new Option("Other", 0x40))), + + AUTOMATIC_TEMPERATURE_CONTROL(0xB1), + NORMAL_HIGH_SPEED_SILENT_OPERATION(0xB2), + SET_TEMPERATURE(0xB3, StateCodec.Temperature8bitCodec.INSTANCE), + SET_RELATIVE_HUMIDITY(0xB4), + SET_TEMPERATURE_COOLING_MODE(0xB5), + SET_TEMPERATURE_HEATING_MODE(0xB6), + SET_TEMPERATURE_DEHUMIDIFYING_MODE(0xB7), + RATED_POWER_CONSUMPTION(0xB8), + MEASURED_CURRENT_CONSUMPTION(0xB9), + MEASURED_ROOM_RELATIVE_HUMIDITY(0xBA), + MEASURED_ROOM_TEMPERATURE(0xBB, StateCodec.Temperature8bitCodec.INSTANCE, null), + SET_TEMPERATURE_USER_REMOTE_CONTROL(0xBC), + MEASURED_COOLED_AIR_TEMPERATURE(0xBD), + MEASURED_OUTDOOR_TEMPERATURE(0xBE, StateCodec.Temperature8bitCodec.INSTANCE, null), + RELATIVE_TEMPERATURE(0xBF); + // @formatter:on + + public final int code; + + @Nullable + public final StateDecode stateDecode; + + @Nullable + public final StateEncode stateEncode; + + AcGroup(int code) { + this(code, null, null); + } + + AcGroup(int code, @Nullable StateDecode stateDecode, @Nullable StateEncode stateEncode) { + this.code = code; + this.stateDecode = stateDecode; + this.stateEncode = stateEncode; + } + + AcGroup(int code, StateCodec stateCodec) { + this(code, stateCodec, stateCodec); + } + + public int code() { + return code; + } + + @Nullable + public StateDecode decoder() { + return stateDecode; + } + + @Nullable + public StateEncode encoder() { + return stateEncode; + } + } + + enum HomeAc implements Epc { + VENTILATION_FUNCTION(0xC0), + HUMIDIFIER_FUNCTION(0xC1), + VENTILATION_AIR_FLOW_RATE(0xC3); + + public final int code; + + HomeAc(int code) { + this.code = code; + } + + public int code() { + return code; + } + } + + enum Profile implements Epc { + OPERATING_STATUS(0x80, new OptionCodec(new Option("Booting", 0x30), new Option("Not booting", 0x31))), + VERSION_INFORMATION(0x82), + NODE_IDENTIFICATION_NUMBER(0x83), + FAULT_CONTENT(0x89); + + public final int code; + + @Nullable + public final StateDecode stateDecode; + @Nullable + public final StateEncode stateEncode; + + Profile(int code) { + this(code, null, null); + } + + Profile(int code, @Nullable StateDecode stateDecode, @Nullable StateEncode stateEncode) { + this.code = code; + this.stateDecode = stateDecode; + this.stateEncode = stateEncode; + } + + Profile(int code, StateCodec stateCodec) { + this(code, stateCodec, stateCodec); + } + + public int code() { + return code; + } + + @Nullable + public StateDecode decoder() { + return stateDecode; + } + + @Nullable + public StateEncode encoder() { + return stateEncode; + } + } + + enum ProfileGroup implements Epc { + UNIQUE_IDENTIFIER_CODE(0xBF); + + public final int code; + + ProfileGroup(int code) { + this.code = code; + } + + public int code() { + return code; + } + } + + enum NodeProfile implements Epc { + EA(0xE0), + NET_ID(0xE1), + NODE_D(0xE2), + DEFAULT_ROUTER_DATA(0xE3), + ALL_ROUTER_DATA(0xE4), + LOCK_CONTROL_STATUS(0xEE), + LOCK_CONTROL_DATA(0xEF), + SECURE_COMMUNICATION_COMMON_KEY_SETUP_USER_KEY(0xC0), + SECURE_COMMUNICATION_COMMON_KEY_SETUP_SERVICE_PROVIDER_KEY(0xC1), + SECURE_COMMUNICATION_COMMON_KEY_SWITCHOVER_SETUP_USER_KEY(0xC2), + SECURE_COMMUNICATION_COMMON_KEY_SWITCHOVER_SETUP_SERVICE_PROVIDER_KEY(0xC3), + SECURE_COMMUNICATION_COMMON_KEY_SERIAL_KEY(0xC4), + SELF_NODE_INSTANCE_LIST_PAGE(0xD0), + SELF_NODE_CLASS_LIST(0xD2), + SELF_NODE_INSTANCE_COUNT(0xD3), + SELF_NODE_CLASS_COUNT(0xD4), + INSTANCE_CHANGE_CLASS_COUNT(0xD5), + SELF_NODE_INSTANCE_LIST_S(0xD6), + SELF_NODE_CLASS_LIST_S(0xD7), + RELATED_TO_OTHER_NODE_EA_LIST(0xD8), + RELATED_TO_OTHER_NODE_EA_COUNT(0xD9), + GROUP_BROADCAST_NUMBER(0xDA),; + + public final int code; + + NodeProfile(int code) { + this.code = code; + } + + public int code() { + return code; + } + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EpcLookupTable.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EpcLookupTable.java new file mode 100644 index 00000000000..37c9009db34 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/EpcLookupTable.java @@ -0,0 +1,86 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import static org.openhab.binding.echonetlite.internal.HexUtil.hex; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +enum EpcLookupTable { + INSTANCE; + + private static final int MAX_ENTRIES = 256; + private final Epc[][][] lookupTable = new Epc[MAX_ENTRIES][0][0]; + + EpcLookupTable() { + addLookupTableEntries(lookupTable, EchonetClass.AIRCON_HOMEAC); + addLookupTableEntries(lookupTable, EchonetClass.MANAGEMENT_CONTROLLER); + addLookupTableEntries(lookupTable, EchonetClass.NODE_PROFILE); + } + + public Epc resolve(int groupCode, int classCode, int epcCode) { + if (MAX_ENTRIES <= groupCode) { + throw new IllegalArgumentException(MAX_ENTRIES + "<= groupCode (" + groupCode + ")"); + } + if (MAX_ENTRIES <= classCode) { + throw new IllegalArgumentException(MAX_ENTRIES + "<= classCode (" + classCode + ")"); + } + if (MAX_ENTRIES <= epcCode) { + throw new IllegalArgumentException(MAX_ENTRIES + "<= epcCode (" + epcCode + ")"); + } + + if (0 == lookupTable[groupCode].length) { + throw new IllegalArgumentException("groupCode (" + hex(groupCode) + ") has no entries"); + } + + if (0 == lookupTable[groupCode][classCode].length) { + throw new IllegalArgumentException( + "groupCode/classCode (" + hex(groupCode) + "/" + hex(classCode) + ") has no entries"); + } + + if (null == lookupTable[groupCode][classCode][epcCode]) { + throw new IllegalArgumentException("groupCode/classCode (" + hex(groupCode) + "/" + hex(classCode) + "/" + + hex(epcCode) + ") has no entry"); + } + + return lookupTable[groupCode][classCode][epcCode]; + } + + private static void addLookupTableEntries(Epc[][][] lookupTable, EchonetClass echonetClass) { + final int groupCode = echonetClass.groupCode(); + final int classCode = echonetClass.classCode(); + + if (null == lookupTable[groupCode] || 0 == lookupTable[groupCode].length) { + lookupTable[groupCode] = new Epc[MAX_ENTRIES][0]; + } + if (null == lookupTable[groupCode][classCode] || 0 == lookupTable[groupCode][classCode].length) { + lookupTable[groupCode][classCode] = new Epc[MAX_ENTRIES]; + } + + for (Epc value : echonetClass.deviceProperties()) { + lookupTable[groupCode][classCode][value.code()] = value; + } + + for (Epc value : echonetClass.groupProperties()) { + lookupTable[groupCode][classCode][value.code()] = value; + } + + for (Epc value : echonetClass.classProperties()) { + lookupTable[groupCode][classCode][value.code()] = value; + } + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/Esv.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/Esv.java new file mode 100644 index 00000000000..3f22002bff5 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/Esv.java @@ -0,0 +1,88 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public enum Esv { + SetI(0x60), + SetC(0x61), + Get(0x62), + INF_REQ(0x63), + SetMI(0x64), + SetMC(0x65), + GetM(0x66), + INFM_REQ(0x67), + AddMI(0x68), + AddMC(0x69), + DelMI(0x6a), + DelMC(0x6b), + CheckM(0x6c), + AddMSI(0x6d), + AddMSC(0x6e), + Set_Res(0x71), + Get_Res(0x72), + INF(0x73), + INFC(0x74), + SetM_Res(0x75), + GetM_Res(0x76), + INFM(0x77), + INFMC(0x78), + AddM_Res(0x79), + INFC_Res(0x7a), + DelM_Res(0x7b), + CheckM_Res(0x7d), + INFMC_Res(0x7d), + AddMS_Res(0x7e), + SetI_SNA(0x50), + SetC_SNA(0x51), + Get_SNA(0x52), + INF_SNA(0x53), + SetMI_SNA(0x54), + SetMC_SNA(0x55), + GetM_SNA(0x56), + INFM_SNA(0x57), + AddMI_SNA(0x58), + AddMC_SNA(0x59), + DelMI_SNA(0x5a), + DelMC_SNA(0x5b), + CheckM_SNA(0x5c), + AddMSI_SNA(0x5d), + AddMSC_SNA(0x5e), + Unknown(0x00); + + private final byte code; + + Esv(int code) { + this.code = (byte) (code & 0xFF); + } + + public static Esv forCode(byte b) { + final Esv[] values = values(); + for (Esv value : values) { + if (value.code == b) { + return value; + } + } + + throw new IllegalArgumentException("Unable to find Esv for: " + b); + } + + public byte code() { + return code; + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/HexUtil.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/HexUtil.java new file mode 100644 index 00000000000..e215a358eab --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/HexUtil.java @@ -0,0 +1,71 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import java.nio.ByteBuffer; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public class HexUtil { + public static String hex(ByteBuffer buffer) { + return hex(buffer, "[", "]", "0x", ","); + } + + public static String hex(final ByteBuffer buffer, final String stringPrefix, final String stringSuffix, + final String bytePrefix, final String delimiter) { + final StringBuilder sb = new StringBuilder(); + sb.append(stringPrefix); + for (int i = buffer.position(), n = buffer.limit(); i < n; i++) { + final int b = buffer.get(i) & 0xFF; + final String prefix = b < 0x10 ? "0" : ""; + sb.append(bytePrefix).append(prefix).append(Integer.toHexString(b)).append(delimiter); + } + sb.setLength(sb.length() - delimiter.length()); + sb.append(stringSuffix); + + return sb.toString(); + } + + public static String hex(int[] array, int offset, int length) { + final StringBuilder sb = new StringBuilder(); + sb.append('['); + for (int i = offset; i < length; i++) { + final int b = array[i] & 0xFF; + hex(sb, b); + sb.append(','); + } + sb.setLength(sb.length() - 1); + sb.append(']'); + + return sb.toString(); + } + + private static void hex(final StringBuilder sb, final int b) { + final String prefix = b < 0x10 ? "0" : ""; + sb.append("0x").append(prefix).append(Integer.toHexString(b)); + } + + public static String hex(final int b) { + final StringBuilder sb = new StringBuilder(); + hex(sb, b); + return sb.toString(); + } + + public static String hex(int[] array) { + return hex(array, 0, array.length); + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/InstanceKey.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/InstanceKey.java new file mode 100644 index 00000000000..b99463625e8 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/InstanceKey.java @@ -0,0 +1,62 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import static java.util.Objects.requireNonNull; +import static org.openhab.binding.echonetlite.internal.HexUtil.hex; + +import java.net.InetSocketAddress; +import java.util.Objects; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public class InstanceKey { + final InetSocketAddress address; + final EchonetClass klass; + final int instance; + + public InstanceKey(final InetSocketAddress address, final EchonetClass klass, final int instance) { + this.address = requireNonNull(address); + this.klass = requireNonNull(klass); + this.instance = instance; + } + + public String toString() { + return "InstanceKey{" + "address=" + address + ", klass=" + klass + ", instance=" + instance + '}'; + } + + public String representationProperty() { + return address.getAddress().getHostAddress() + "_" + hex(klass.groupCode()) + ":" + hex(klass.classCode()) + ":" + + hex(instance); + } + + public boolean equals(@Nullable final Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + final InstanceKey that = (InstanceKey) o; + return instance == that.instance && address.equals(that.address) && klass == that.klass; + } + + public int hashCode() { + return Objects.hash(address, klass, instance); + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/LangUtil.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/LangUtil.java new file mode 100644 index 00000000000..60c7184ba29 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/LangUtil.java @@ -0,0 +1,42 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public class LangUtil { + public static byte b(int i) { + return (byte) (i & 0xFF); + } + + public static String constantToVariable(CharSequence constant) { + final StringBuilder sb = new StringBuilder(); + boolean shouldCapitalise = false; + for (int i = 0, n = constant.length(); i < n; i++) { + final char c = constant.charAt(i); + if ('_' == c) { + shouldCapitalise = true; + } else if (shouldCapitalise) { + sb.append(Character.toUpperCase(c)); + shouldCapitalise = false; + } else { + sb.append(Character.toLowerCase(c)); + } + } + return sb.toString(); + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/MonotonicClock.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/MonotonicClock.java new file mode 100644 index 00000000000..f807a1a3c49 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/MonotonicClock.java @@ -0,0 +1,33 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +class MonotonicClock { + private final long baseTimeNs; + + MonotonicClock() { + baseTimeNs = System.nanoTime(); + } + + long timeMs() { + return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - baseTimeNs); + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/ShortSupplier.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/ShortSupplier.java new file mode 100644 index 00000000000..d2e8dbf2c09 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/ShortSupplier.java @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import org.eclipse.jdt.annotation.NonNullByDefault; + +/** + * @author Michael Barker - Initial contribution + */ +@FunctionalInterface +@NonNullByDefault +public interface ShortSupplier { + short getAsShort(); +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/StateCodec.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/StateCodec.java new file mode 100644 index 00000000000..b4adf8409de --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/StateCodec.java @@ -0,0 +1,216 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import static org.openhab.binding.echonetlite.internal.HexUtil.hex; +import static org.openhab.binding.echonetlite.internal.LangUtil.b; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.eclipse.jdt.annotation.Nullable; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.library.unit.Units; +import org.openhab.core.types.State; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public interface StateCodec extends StateEncode, StateDecode { + + class OnOffCodec implements StateCodec { + private final int on; + private final int off; + + public OnOffCodec(int on, int off) { + this.on = on; + this.off = off; + } + + public State decodeState(final ByteBuffer edt) { + return b(on) == edt.get() ? OnOffType.ON : OnOffType.OFF; + } + + public void encodeState(final State state, final ByteBuffer edt) { + final OnOffType onOff = (OnOffType) state; + edt.put(onOff == OnOffType.ON ? b(on) : b(off)); + } + + public String itemType() { + return "Switch"; + } + } + + enum StandardVersionInformationCodec implements StateDecode { + + INSTANCE; + + public State decodeState(final ByteBuffer edt) { + final int pdc = edt.remaining(); + if (pdc != 4) { + return StringType.EMPTY; + } + + return new StringType("" + (char) edt.get(edt.position() + 2)); + } + + public String itemType() { + return "String"; + } + } + + enum HexStringCodec implements StateDecode { + + INSTANCE; + + public State decodeState(final ByteBuffer edt) { + return new StringType(hex(edt, "", "", "", "")); + } + + public String itemType() { + return "String"; + } + } + + enum OperatingTimeDecode implements StateDecode { + INSTANCE; + + public State decodeState(final ByteBuffer edt) { + // Specification isn't explicit about byte order, but seems to be work with testing. + edt.order(ByteOrder.BIG_ENDIAN); + + final int b0 = edt.get() & 0xFF; + final long time = edt.getInt() & 0xFFFFFFFFL; + + final TimeUnit timeUnit; + switch (b0) { + case 0x42: + timeUnit = TimeUnit.MINUTES; + break; + + case 0x43: + timeUnit = TimeUnit.HOURS; + break; + + case 0x44: + timeUnit = TimeUnit.DAYS; + break; + + case 0x41: + default: + timeUnit = TimeUnit.SECONDS; + break; + } + + return new QuantityType<>(timeUnit.toSeconds(time), Units.SECOND); + } + + public String itemType() { + return "Number:Time"; + } + } + + class Option { + final String name; + final int value; + final StringType state; + + public Option(final String name, final int value) { + this.name = name; + this.value = value; + this.state = new StringType(name); + } + } + + class OptionCodec implements StateCodec { + + private final Logger logger = LoggerFactory.getLogger(OptionCodec.class); + private final Map optionByName = new HashMap<>(); + private final Option[] optionByValue = new Option[256]; // All options values are single bytes on the wire + private final StringType unknown = new StringType("Unknown"); + + public OptionCodec(Option... options) { + for (Option option : options) { + optionByName.put(option.name, option); + optionByValue[option.value] = option; + } + } + + public String itemType() { + return "String"; + } + + public State decodeState(final ByteBuffer edt) { + final int value = edt.get() & 0xFF; + final Option option = optionByValue[value]; + return null != option ? option.state : unknown; + } + + public void encodeState(final State state, final ByteBuffer edt) { + final Option option = optionByName.get(state.toFullString()); + if (null != option) { + edt.put(b(option.value)); + } else { + logger.warn("No option specified for: {}", state); + } + } + } + + enum Decimal8bitCodec implements StateCodec { + + INSTANCE; + + public String itemType() { + return "Number"; + } + + public State decodeState(final ByteBuffer edt) { + final int value = edt.get(); // Should expand to typed value (mask excluded) + return new DecimalType(value); + } + + public void encodeState(final State state, final ByteBuffer edt) { + edt.put((byte) (((DecimalType) state).intValue())); + } + } + + enum Temperature8bitCodec implements StateCodec { + INSTANCE; + + public State decodeState(final ByteBuffer edt) { + final int value = edt.get(); + return new QuantityType<>(value, SIUnits.CELSIUS); + } + + public String itemType() { + return "Number:Temperature"; + } + + public void encodeState(final State state, final ByteBuffer edt) { + final @Nullable QuantityType tempCelsius = ((QuantityType) state).toUnit(SIUnits.CELSIUS); + edt.put((byte) (Objects.requireNonNull(tempCelsius).intValue())); + } + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/StateDecode.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/StateDecode.java new file mode 100644 index 00000000000..580c810ca90 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/StateDecode.java @@ -0,0 +1,28 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import java.nio.ByteBuffer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.types.State; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +public interface StateDecode { + State decodeState(final ByteBuffer edt); + + String itemType(); +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/StateEncode.java b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/StateEncode.java new file mode 100644 index 00000000000..94f15f9def0 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/java/org/openhab/binding/echonetlite/internal/StateEncode.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import java.nio.ByteBuffer; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.openhab.core.types.State; + +/** + * @author Michael Barker - Initial contribution + */ +@FunctionalInterface +@NonNullByDefault +public interface StateEncode { + void encodeState(final State state, final ByteBuffer edt); +} diff --git a/bundles/org.openhab.binding.echonetlite/src/main/resources/OH-INF/binding/binding.xml b/bundles/org.openhab.binding.echonetlite/src/main/resources/OH-INF/binding/binding.xml new file mode 100644 index 00000000000..c69e0af6eef --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/resources/OH-INF/binding/binding.xml @@ -0,0 +1,9 @@ + + + + EchonetLite Binding + This is the binding for EchonetLite. + + diff --git a/bundles/org.openhab.binding.echonetlite/src/main/resources/OH-INF/i18n/echonetlite.properties b/bundles/org.openhab.binding.echonetlite/src/main/resources/OH-INF/i18n/echonetlite.properties new file mode 100644 index 00000000000..c1f4762b7e5 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/resources/OH-INF/i18n/echonetlite.properties @@ -0,0 +1,251 @@ +# binding + +binding.echonetlite.name = EchonetLite Binding +binding.echonetlite.description = This is the binding for EchonetLite. + +# thing types + +thing-type.echonetlite.bridge.label = Echonet Bridge +thing-type.echonetlite.bridge.description = Virtual bridge to ensure that there is only a single binding to the echonet port +thing-type.echonetlite.device.label = EchonetLite Device +thing-type.echonetlite.device.description = Device for EchonetLite Binding + +# thing types config + +thing-type.config.echonetlite.bridge.multicastAddress.label = Discovery/Notification Address +thing-type.config.echonetlite.bridge.multicastAddress.description = Address used to discover nodes and receive notifications +thing-type.config.echonetlite.bridge.port.label = Echonet Port +thing-type.config.echonetlite.bridge.port.description = Port used for echonet messages both outbound and inbound +thing-type.config.echonetlite.device.classCode.label = Class Code +thing-type.config.echonetlite.device.classCode.description = Echonet Class Code +thing-type.config.echonetlite.device.groupCode.label = Group Code +thing-type.config.echonetlite.device.groupCode.description = Echonet Group Code +thing-type.config.echonetlite.device.hostname.label = Hostname +thing-type.config.echonetlite.device.hostname.description = Hostname or IP address of the device +thing-type.config.echonetlite.device.instance.label = Instance +thing-type.config.echonetlite.device.instance.description = Echonet Instance +thing-type.config.echonetlite.device.pollIntervalMs.label = Poll Interval (ms) +thing-type.config.echonetlite.device.pollIntervalMs.description = Interval in ms between each poll of the device +thing-type.config.echonetlite.device.port.label = Port +thing-type.config.echonetlite.device.port.description = Port of the device (usually 3610) +thing-type.config.echonetlite.device.retryTimeoutMs.label = Retry Timeout (ms) +thing-type.config.echonetlite.device.retryTimeoutMs.description = Timeout in ms before a message is resent + +# channel types + +channel-type.echonetlite.airFlowDirectionHorizontal.label = Air Flow Direction Horizontal +channel-type.echonetlite.airFlowDirectionHorizontal.description = Air Flow Direction Horizontal +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.XXXOO = XXXOO +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.OOXXX = OOXXX +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.XOOOX = XOOOX +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.OOXOO = OOXOO +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.XXXXO = XXXXO +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.XXXOX = XXXOX +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.XXOXX = XXOXX +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.XXOXO = XXOXO +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.XXOOX = XXOOX +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.XXOOO = XXOOO +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.XOXXX = XOXXX +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.XOXXO = XOXXO +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.XOXOX = XOXOX +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.XOXOO = XOXOO +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.XOOXX = XOOXX +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.XOOXO = XOOXO +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.XOOOO = XOOOO +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.OXXXX = OXXXX +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.OXXXO = OXXXO +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.OXXOX = OXXOX +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.OXXOO = OXXOO +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.OXOXX = OXOXX +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.OXOXO = OXOXO +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.OXOOX = OXOOX +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.OXOOO = OXOOO +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.OOXXO = OOXXO +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.OOXOX = OOXOX +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.OOOXX = OOOXX +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.OOOXO = OOOXO +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.OOOOX = OOOOX +channel-type.echonetlite.airFlowDirectionHorizontal.state.option.OOOOO = OOOOO +channel-type.echonetlite.airFlowDirectionVertical.label = Air Flow Direction Vertical +channel-type.echonetlite.airFlowDirectionVertical.description = Air Flow Direction Vertical +channel-type.echonetlite.airFlowDirectionVertical.state.option.Uppermost = Uppermost +channel-type.echonetlite.airFlowDirectionVertical.state.option.Lowermost = Lowermost +channel-type.echonetlite.airFlowDirectionVertical.state.option.Mid-uppermost = Mid-uppermost +channel-type.echonetlite.airFlowDirectionVertical.state.option.Mid-lowermost = Mid-lowermost +channel-type.echonetlite.airFlowDirectionVertical.state.option.Central = Central +channel-type.echonetlite.airFlowRate.label = Air Flow Rate +channel-type.echonetlite.airFlowRate.description = Air Flow Rate +channel-type.echonetlite.airFlowRate.state.option.Auto = Auto +channel-type.echonetlite.airFlowRate.state.option.Rate\ 1 = Rate 1 +channel-type.echonetlite.airFlowRate.state.option.Rate\ 2 = Rate 2 +channel-type.echonetlite.airFlowRate.state.option.Rate\ 3 = Rate 3 +channel-type.echonetlite.airFlowRate.state.option.Rate\ 4 = Rate 4 +channel-type.echonetlite.airFlowRate.state.option.Rate\ 5 = Rate 5 +channel-type.echonetlite.airFlowRate.state.option.Rate\ 6 = Rate 6 +channel-type.echonetlite.airFlowRate.state.option.Rate\ 7 = Rate 7 +channel-type.echonetlite.airFlowRate.state.option.Rate\ 8 = Rate 8 +channel-type.echonetlite.automaticControlOfAirFlowDirection.label = Automatic Air Flow Direction +channel-type.echonetlite.automaticControlOfAirFlowDirection.description = The type of automatic control applied to the air flow direction, if any +channel-type.echonetlite.automaticControlOfAirFlowDirection.state.option.Automatic = Automatic +channel-type.echonetlite.automaticControlOfAirFlowDirection.state.option.Non-automatic = Non-automatic +channel-type.echonetlite.automaticControlOfAirFlowDirection.state.option.Automatic\ (vertical) = Automatic (vertical) +channel-type.echonetlite.automaticControlOfAirFlowDirection.state.option.Automatic\ (horizontal) = Automatic (horizontal) +channel-type.echonetlite.automaticSwingOfAirFlow.label = Automatic Swing Of Air Flow +channel-type.echonetlite.automaticSwingOfAirFlow.description = Automatic Swing Of Air Flow +channel-type.echonetlite.automaticSwingOfAirFlow.state.option.Not\ used = Not used +channel-type.echonetlite.automaticSwingOfAirFlow.state.option.Used\ (vertical) = Used (vertical) +channel-type.echonetlite.automaticSwingOfAirFlow.state.option.Used\ (horizontal) = Used (horizontal) +channel-type.echonetlite.automaticSwingOfAirFlow.state.option.Used\ (vertical\ and\ horizontal) = Used (vertical and horizontal) +channel-type.echonetlite.businessFacilityCode.label = Business Facility Code +channel-type.echonetlite.businessFacilityCode.description = Business Facility Code +channel-type.echonetlite.cumulativeOperatingTime.label = Cumulative Operating Time +channel-type.echonetlite.cumulativeOperatingTime.description = Cumulative time the unit has been operating in seconds +channel-type.echonetlite.faultDescription.label = Fault Description +channel-type.echonetlite.faultDescription.description = Fault Description +channel-type.echonetlite.faultStatus.label = Fault Status +channel-type.echonetlite.faultStatus.description = Fault Status +channel-type.echonetlite.identificationNumber.label = Identification Number +channel-type.echonetlite.identificationNumber.description = Identification Number +channel-type.echonetlite.installationLocation.label = Installation Location +channel-type.echonetlite.installationLocation.description = Installation Location +channel-type.echonetlite.installationLocation.state.option.Not\ specified = Not specified +channel-type.echonetlite.installationLocation.state.option.Living\ Room = Living Room +channel-type.echonetlite.installationLocation.state.option.Living\ Room\ 1 = Living Room 1 +channel-type.echonetlite.installationLocation.state.option.Living\ Room\ 2 = Living Room 2 +channel-type.echonetlite.installationLocation.state.option.Living\ Room\ 3 = Living Room 3 +channel-type.echonetlite.installationLocation.state.option.Living\ Room\ 4 = Living Room 4 +channel-type.echonetlite.installationLocation.state.option.Living\ Room\ 5 = Living Room 5 +channel-type.echonetlite.installationLocation.state.option.Living\ Room\ 6 = Living Room 6 +channel-type.echonetlite.installationLocation.state.option.Living\ Room\ 7 = Living Room 7 +channel-type.echonetlite.installationLocation.state.option.Dining\ Room = Dining Room +channel-type.echonetlite.installationLocation.state.option.Dining\ Room\ 1 = Dining Room 1 +channel-type.echonetlite.installationLocation.state.option.Dining\ Room\ 2 = Dining Room 2 +channel-type.echonetlite.installationLocation.state.option.Dining\ Room\ 3 = Dining Room 3 +channel-type.echonetlite.installationLocation.state.option.Dining\ Room\ 4 = Dining Room 4 +channel-type.echonetlite.installationLocation.state.option.Dining\ Room\ 5 = Dining Room 5 +channel-type.echonetlite.installationLocation.state.option.Dining\ Room\ 6 = Dining Room 6 +channel-type.echonetlite.installationLocation.state.option.Dining\ Room\ 7 = Dining Room 7 +channel-type.echonetlite.installationLocation.state.option.Kitchen = "Kitchen" +channel-type.echonetlite.installationLocation.state.option.Kitchen\ 1 = Kitchen 1 +channel-type.echonetlite.installationLocation.state.option.Kitchen\ 2 = Kitchen 2 +channel-type.echonetlite.installationLocation.state.option.Kitchen\ 3 = Kitchen 3 +channel-type.echonetlite.installationLocation.state.option.Kitchen\ 4 = Kitchen 4 +channel-type.echonetlite.installationLocation.state.option.Kitchen\ 5 = Kitchen 5 +channel-type.echonetlite.installationLocation.state.option.Kitchen\ 6 = Kitchen 6 +channel-type.echonetlite.installationLocation.state.option.Kitchen\ 7 = Kitchen 7 +channel-type.echonetlite.installationLocation.state.option.Lavatory = "Lavatory" +channel-type.echonetlite.installationLocation.state.option.Lavatory\ 1 = Lavatory 1 +channel-type.echonetlite.installationLocation.state.option.Lavatory\ 2 = Lavatory 2 +channel-type.echonetlite.installationLocation.state.option.Lavatory\ 3 = Lavatory 3 +channel-type.echonetlite.installationLocation.state.option.Lavatory\ 4 = Lavatory 4 +channel-type.echonetlite.installationLocation.state.option.Lavatory\ 5 = Lavatory 5 +channel-type.echonetlite.installationLocation.state.option.Lavatory\ 6 = Lavatory 6 +channel-type.echonetlite.installationLocation.state.option.Lavatory\ 7 = Lavatory 7 +channel-type.echonetlite.installationLocation.state.option.Washroom/changing\ room = Washroom/changing room +channel-type.echonetlite.installationLocation.state.option.Washroom/changing\ room\ 1 = Washroom/changing room 1 +channel-type.echonetlite.installationLocation.state.option.Washroom/changing\ room\ 2 = Washroom/changing room 2 +channel-type.echonetlite.installationLocation.state.option.Washroom/changing\ room\ 3 = Washroom/changing room 3 +channel-type.echonetlite.installationLocation.state.option.Washroom/changing\ room\ 4 = Washroom/changing room 4 +channel-type.echonetlite.installationLocation.state.option.Washroom/changing\ room\ 5 = Washroom/changing room 5 +channel-type.echonetlite.installationLocation.state.option.Washroom/changing\ room\ 6 = Washroom/changing room 6 +channel-type.echonetlite.installationLocation.state.option.Washroom/changing\ room\ 7 = Washroom/changing room 7 +channel-type.echonetlite.installationLocation.state.option.Passageway = "Passageway" +channel-type.echonetlite.installationLocation.state.option.Passageway\ 1 = Passageway 1 +channel-type.echonetlite.installationLocation.state.option.Passageway\ 2 = Passageway 2 +channel-type.echonetlite.installationLocation.state.option.Passageway\ 3 = Passageway 3 +channel-type.echonetlite.installationLocation.state.option.Passageway\ 4 = Passageway 4 +channel-type.echonetlite.installationLocation.state.option.Passageway\ 5 = Passageway 5 +channel-type.echonetlite.installationLocation.state.option.Passageway\ 6 = Passageway 6 +channel-type.echonetlite.installationLocation.state.option.Passageway\ 7 = Passageway 7 +channel-type.echonetlite.installationLocation.state.option.Room = "Room" +channel-type.echonetlite.installationLocation.state.option.Room\ 1 = Room 1 +channel-type.echonetlite.installationLocation.state.option.Room\ 2 = Room 2 +channel-type.echonetlite.installationLocation.state.option.Room\ 3 = Room 3 +channel-type.echonetlite.installationLocation.state.option.Room\ 4 = Room 4 +channel-type.echonetlite.installationLocation.state.option.Room\ 5 = Room 5 +channel-type.echonetlite.installationLocation.state.option.Room\ 6 = Room 6 +channel-type.echonetlite.installationLocation.state.option.Room\ 7 = Room 7 +channel-type.echonetlite.installationLocation.state.option.Stairway = "Stairway" +channel-type.echonetlite.installationLocation.state.option.Stairway\ 1 = Stairway 1 +channel-type.echonetlite.installationLocation.state.option.Stairway\ 2 = Stairway 2 +channel-type.echonetlite.installationLocation.state.option.Stairway\ 3 = Stairway 3 +channel-type.echonetlite.installationLocation.state.option.Stairway\ 4 = Stairway 4 +channel-type.echonetlite.installationLocation.state.option.Stairway\ 5 = Stairway 5 +channel-type.echonetlite.installationLocation.state.option.Stairway\ 6 = Stairway 6 +channel-type.echonetlite.installationLocation.state.option.Stairway\ 7 = Stairway 7 +channel-type.echonetlite.installationLocation.state.option.Front\ door = Front door +channel-type.echonetlite.installationLocation.state.option.Front\ door\ 1 = Front door 1 +channel-type.echonetlite.installationLocation.state.option.Front\ door\ 2 = Front door 2 +channel-type.echonetlite.installationLocation.state.option.Front\ door\ 3 = Front door 3 +channel-type.echonetlite.installationLocation.state.option.Front\ door\ 4 = Front door 4 +channel-type.echonetlite.installationLocation.state.option.Front\ door\ 5 = Front door 5 +channel-type.echonetlite.installationLocation.state.option.Front\ door\ 6 = Front door 6 +channel-type.echonetlite.installationLocation.state.option.Front\ door\ 7 = Front door 7 +channel-type.echonetlite.installationLocation.state.option.Storeroom = "Storeroom" +channel-type.echonetlite.installationLocation.state.option.Storeroom\ 1 = Storeroom 1 +channel-type.echonetlite.installationLocation.state.option.Storeroom\ 2 = Storeroom 2 +channel-type.echonetlite.installationLocation.state.option.Storeroom\ 3 = Storeroom 3 +channel-type.echonetlite.installationLocation.state.option.Storeroom\ 4 = Storeroom 4 +channel-type.echonetlite.installationLocation.state.option.Storeroom\ 5 = Storeroom 5 +channel-type.echonetlite.installationLocation.state.option.Storeroom\ 6 = Storeroom 6 +channel-type.echonetlite.installationLocation.state.option.Storeroom\ 7 = Storeroom 7 +channel-type.echonetlite.installationLocation.state.option.Garden/perimeter = Garden/perimeter +channel-type.echonetlite.installationLocation.state.option.Garden/perimeter\ 1 = Garden/perimeter 1 +channel-type.echonetlite.installationLocation.state.option.Garden/perimeter\ 2 = Garden/perimeter 2 +channel-type.echonetlite.installationLocation.state.option.Garden/perimeter\ 3 = Garden/perimeter 3 +channel-type.echonetlite.installationLocation.state.option.Garden/perimeter\ 4 = Garden/perimeter 4 +channel-type.echonetlite.installationLocation.state.option.Garden/perimeter\ 5 = Garden/perimeter 5 +channel-type.echonetlite.installationLocation.state.option.Garden/perimeter\ 6 = Garden/perimeter 6 +channel-type.echonetlite.installationLocation.state.option.Garden/perimeter\ 7 = Garden/perimeter 7 +channel-type.echonetlite.installationLocation.state.option.Garage = "Garage" +channel-type.echonetlite.installationLocation.state.option.Garage\ 1 = Garage 1 +channel-type.echonetlite.installationLocation.state.option.Garage\ 2 = Garage 2 +channel-type.echonetlite.installationLocation.state.option.Garage\ 3 = Garage 3 +channel-type.echonetlite.installationLocation.state.option.Garage\ 4 = Garage 4 +channel-type.echonetlite.installationLocation.state.option.Garage\ 5 = Garage 5 +channel-type.echonetlite.installationLocation.state.option.Garage\ 6 = Garage 6 +channel-type.echonetlite.installationLocation.state.option.Garage\ 7 = Garage 7 +channel-type.echonetlite.installationLocation.state.option.Veranda/balcony = Veranda/balcony +channel-type.echonetlite.installationLocation.state.option.Veranda/balcony\ 1 = Veranda/balcony 1 +channel-type.echonetlite.installationLocation.state.option.Veranda/balcony\ 2 = Veranda/balcony 2 +channel-type.echonetlite.installationLocation.state.option.Veranda/balcony\ 3 = Veranda/balcony 3 +channel-type.echonetlite.installationLocation.state.option.Veranda/balcony\ 4 = Veranda/balcony 4 +channel-type.echonetlite.installationLocation.state.option.Veranda/balcony\ 5 = Veranda/balcony 5 +channel-type.echonetlite.installationLocation.state.option.Veranda/balcony\ 6 = Veranda/balcony 6 +channel-type.echonetlite.installationLocation.state.option.Veranda/balcony\ 7 = Veranda/balcony 7 +channel-type.echonetlite.installationLocation.state.option.Others = "Others" +channel-type.echonetlite.installationLocation.state.option.Others\ 1 = Others 1 +channel-type.echonetlite.installationLocation.state.option.Others\ 2 = Others 2 +channel-type.echonetlite.installationLocation.state.option.Others\ 3 = Others 3 +channel-type.echonetlite.installationLocation.state.option.Others\ 4 = Others 4 +channel-type.echonetlite.installationLocation.state.option.Others\ 5 = Others 5 +channel-type.echonetlite.installationLocation.state.option.Others\ 6 = Others 6 +channel-type.echonetlite.installationLocation.state.option.Others\ 7 = Others 7 +channel-type.echonetlite.manufacturerCode.label = Manufacturer Code +channel-type.echonetlite.manufacturerCode.description = Manufacturer Code +channel-type.echonetlite.manufacturerFaultCode.label = Manufacturer Fault Code +channel-type.echonetlite.manufacturerFaultCode.description = Manufacturer Fault Code +channel-type.echonetlite.measuredOutdoorTemperature.label = Measured Outdoor Temperature +channel-type.echonetlite.measuredOutdoorTemperature.description = Measured Outdoor Temperature +channel-type.echonetlite.measuredRoomTemperature.label = Measured Room Temperature +channel-type.echonetlite.measuredRoomTemperature.description = Measured Room Temperature +channel-type.echonetlite.operationMode.label = Operation Mode +channel-type.echonetlite.operationMode.description = The current mode for the Home AC unit (heating, cooling, etc.) +channel-type.echonetlite.operationMode.state.option.Automatic = Automatic +channel-type.echonetlite.operationMode.state.option.Cooling = Cooling +channel-type.echonetlite.operationMode.state.option.Heating = Heating +channel-type.echonetlite.operationMode.state.option.Dry = Dry +channel-type.echonetlite.operationMode.state.option.Fan = Fan +channel-type.echonetlite.operationMode.state.option.Other = Other +channel-type.echonetlite.operationStatus.label = Operation Status +channel-type.echonetlite.operationStatus.description = Operation Status +channel-type.echonetlite.powerSavingOperationSetting.label = Power Saving +channel-type.echonetlite.powerSavingOperationSetting.description = Controls whether the unit is in power saving operation or not +channel-type.echonetlite.setTemperature.label = Set Temperature +channel-type.echonetlite.setTemperature.description = Desired target room temperature +channel-type.echonetlite.standardVersionInformation.label = Standard Version Information +channel-type.echonetlite.standardVersionInformation.description = Standard Version Information + +# thing status descriptions + +offline.conf-error.null-bridge-handler = Bridge is null diff --git a/bundles/org.openhab.binding.echonetlite/src/main/resources/OH-INF/thing/channel-types.xml b/bundles/org.openhab.binding.echonetlite/src/main/resources/OH-INF/thing/channel-types.xml new file mode 100644 index 00000000000..84da5eeaaba --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/resources/OH-INF/thing/channel-types.xml @@ -0,0 +1,378 @@ + + + + + Switch + + Operation Status + Switch + + + + String + + Installation Location + Text + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String + + Standard Version Information + Text + + + + + String + + Identification Number + Text + + + + + String + + Manufacturer Fault Code + Text + + + + + Switch + + Fault Status + Alarm + + + + + String + + Fault Description + Text + + + + + String + + Manufacturer Code + Text + + + + + String + + Business Facility Code + Text + + + + + Switch + + Controls whether the unit is in power saving operation or not + Switch + + + + Number:Time + + Cumulative time the unit has been operating in seconds + Time + + + + String + + Air Flow Rate + Flow + + + + + + + + + + + + + + + + + String + + The type of automatic control applied to the air flow direction, if any + + + + + + + + + + + + String + + Automatic Swing Of Air Flow + + + + + + + + + + + + String + + Air Flow Direction Vertical + + + + + + + + + + + + + String + + Air Flow Direction Horizontal + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + String + + The current mode for the Home AC unit (heating, cooling, etc.) + + + + + + + + + + + + + + Number:Temperature + + Desired target room temperature + Temperature + + Setpoint + Temperature + + + + + + Number:Temperature + + Measured Room Temperature + Temperature + + Measurement + Temperature + + + + + + Number:Temperature + + Measured Outdoor Temperature + Temperature + + Measurement + Temperature + + + + + diff --git a/bundles/org.openhab.binding.echonetlite/src/main/resources/OH-INF/thing/thing-types.xml b/bundles/org.openhab.binding.echonetlite/src/main/resources/OH-INF/thing/thing-types.xml new file mode 100644 index 00000000000..3b90dea0803 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/main/resources/OH-INF/thing/thing-types.xml @@ -0,0 +1,73 @@ + + + + + + Virtual bridge to ensure that there is only a single binding to the echonet port + + port + + + + network-address + + Address used to discover nodes and receive notifications + 224.0.23.0 + + + + Port used for echonet messages both outbound and inbound + 3610 + + + + + + + + + + + Device for EchonetLite Binding + instanceKey + + + + network-address + + Hostname or IP address of the device + + + 3610 + + Port of the device (usually 3610) + + + + Echonet Group Code + + + + Echonet Class Code + + + + Echonet Instance + + + 30000 + + Interval in ms between each poll of the device + + + 2000 + + Timeout in ms before a message is resent + + + + + diff --git a/bundles/org.openhab.binding.echonetlite/src/test/java/org/openhab/binding/echonetlite/internal/EpcTest.java b/bundles/org.openhab.binding.echonetlite/src/test/java/org/openhab/binding/echonetlite/internal/EpcTest.java new file mode 100644 index 00000000000..c6cf26e2d1f --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/test/java/org/openhab/binding/echonetlite/internal/EpcTest.java @@ -0,0 +1,43 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.openhab.binding.echonetlite.internal.EchonetClass.AIRCON_HOMEAC; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +class EpcTest { + + @Test + void shouldLookupEpc() { + final EchonetClass echonetClass = AIRCON_HOMEAC; + + for (Epc epc : Epc.Device.values()) { + assertEquals(epc, Epc.lookup(echonetClass.groupCode(), echonetClass.classCode(), epc.code())); + } + + for (Epc epc : Epc.AcGroup.values()) { + assertEquals(epc, Epc.lookup(echonetClass.groupCode(), echonetClass.classCode(), epc.code())); + } + + for (Epc epc : Epc.HomeAc.values()) { + assertEquals(epc, Epc.lookup(echonetClass.groupCode(), echonetClass.classCode(), epc.code())); + } + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/test/java/org/openhab/binding/echonetlite/internal/protocol/LangUtilTest.java b/bundles/org.openhab.binding.echonetlite/src/test/java/org/openhab/binding/echonetlite/internal/protocol/LangUtilTest.java new file mode 100644 index 00000000000..a0b8f08c535 --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/test/java/org/openhab/binding/echonetlite/internal/protocol/LangUtilTest.java @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal.protocol; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.openhab.binding.echonetlite.internal.LangUtil.constantToVariable; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +class LangUtilTest { + + @Test + void shouldConvertConstantToVariable() { + assertEquals("operationStatus", constantToVariable("OPERATION_STATUS")); + } +} diff --git a/bundles/org.openhab.binding.echonetlite/src/test/java/org/openhab/binding/echonetlite/internal/protocol/StateCodecTest.java b/bundles/org.openhab.binding.echonetlite/src/test/java/org/openhab/binding/echonetlite/internal/protocol/StateCodecTest.java new file mode 100644 index 00000000000..7d070f877bd --- /dev/null +++ b/bundles/org.openhab.binding.echonetlite/src/test/java/org/openhab/binding/echonetlite/internal/protocol/StateCodecTest.java @@ -0,0 +1,130 @@ +/** + * Copyright (c) 2010-2022 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.echonetlite.internal.protocol; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.openhab.binding.echonetlite.internal.LangUtil.b; + +import java.nio.ByteBuffer; +import java.nio.ByteOrder; +import java.util.concurrent.TimeUnit; + +import org.eclipse.jdt.annotation.NonNullByDefault; +import org.junit.jupiter.api.Test; +import org.openhab.binding.echonetlite.internal.StateCodec; +import org.openhab.binding.echonetlite.internal.StateCodec.HexStringCodec; +import org.openhab.binding.echonetlite.internal.StateCodec.OperatingTimeDecode; +import org.openhab.binding.echonetlite.internal.StateCodec.Option; +import org.openhab.binding.echonetlite.internal.StateCodec.OptionCodec; +import org.openhab.binding.echonetlite.internal.StateCodec.StandardVersionInformationCodec; +import org.openhab.binding.echonetlite.internal.StateDecode; +import org.openhab.core.library.types.DecimalType; +import org.openhab.core.library.types.OnOffType; +import org.openhab.core.library.types.QuantityType; +import org.openhab.core.library.types.StringType; +import org.openhab.core.library.unit.SIUnits; +import org.openhab.core.types.State; + +/** + * @author Michael Barker - Initial contribution + */ +@NonNullByDefault +class StateCodecTest { + private void assertEncodeDecode(StateCodec stateCodec, State state, byte[] expectedOutput) { + final ByteBuffer buffer = ByteBuffer.allocate(1024); + stateCodec.encodeState(state, buffer); + buffer.flip(); + + final byte[] encoded = new byte[buffer.remaining()]; + buffer.get(encoded); + assertArrayEquals(expectedOutput, encoded); + + buffer.flip(); + + assertEquals(state, stateCodec.decodeState(buffer)); + } + + private void assertDecode(StateDecode stateDecode, State expectedState, byte[] data) { + assertEquals(expectedState, stateDecode.decodeState(ByteBuffer.wrap(data))); + } + + @Test + void shouldEncodeOnOff() { + final int on = 34; + final int off = 27; + final StateCodec.OnOffCodec onOffCodec = new StateCodec.OnOffCodec(on, off); + + assertEncodeDecode(onOffCodec, OnOffType.ON, new byte[] { b(on) }); + assertEncodeDecode(onOffCodec, OnOffType.OFF, new byte[] { b(off) }); + } + + @Test + void shouldDecodeStandardVersionInformation() { + assertDecode(StandardVersionInformationCodec.INSTANCE, StringType.EMPTY, new byte[0]); + assertDecode(StandardVersionInformationCodec.INSTANCE, StringType.EMPTY, new byte[1]); + assertDecode(StandardVersionInformationCodec.INSTANCE, StringType.EMPTY, new byte[2]); + assertDecode(StandardVersionInformationCodec.INSTANCE, StringType.EMPTY, new byte[3]); + assertDecode(StandardVersionInformationCodec.INSTANCE, StringType.EMPTY, new byte[5]); + assertDecode(StandardVersionInformationCodec.INSTANCE, new StringType("A"), new byte[] { 0, 0, 'A', 0 }); + assertDecode(StandardVersionInformationCodec.INSTANCE, new StringType("Z"), new byte[] { 0, 0, 'Z', 0 }); + } + + @Test + void shouldDecodeHexString() { + assertDecode(HexStringCodec.INSTANCE, new StringType("000102030467"), new byte[] { 0, 1, 2, 3, 4, b(0x67) }); + } + + @Test + void shouldDecodeCumulativeOperatingTime() { + final ByteBuffer buffer = ByteBuffer.wrap(new byte[5]); + buffer.order(ByteOrder.BIG_ENDIAN); + + final int valueInSeconds = 484260; + final long valueInMinutes = TimeUnit.SECONDS.toMinutes(valueInSeconds); + buffer.put(b(0x42)); + buffer.putInt((int) valueInMinutes); + + buffer.flip(); + buffer.order(ByteOrder.LITTLE_ENDIAN); + assertEquals(valueInSeconds, ((QuantityType) OperatingTimeDecode.INSTANCE.decodeState(buffer)).intValue()); + + buffer.flip(); + buffer.order(ByteOrder.BIG_ENDIAN); + assertEquals(valueInSeconds, ((QuantityType) OperatingTimeDecode.INSTANCE.decodeState(buffer)).intValue()); + } + + @Test + void shouldEncodeDecodeOption() { + final OptionCodec optionCodec = new OptionCodec(new Option("ABC", 123), new Option("DEF", 101)); + assertEncodeDecode(optionCodec, new StringType("ABC"), new byte[] { 123 }); + assertEncodeDecode(optionCodec, new StringType("DEF"), new byte[] { 101 }); + } + + @Test + void shouldEncodeAndDecode8Bit() { + assertEncodeDecode(StateCodec.Decimal8bitCodec.INSTANCE, new DecimalType(123), new byte[] { 123 }); + assertEncodeDecode(StateCodec.Decimal8bitCodec.INSTANCE, new DecimalType(1), new byte[] { 1 }); + assertEncodeDecode(StateCodec.Decimal8bitCodec.INSTANCE, new DecimalType(-1), new byte[] { b(255) }); + } + + @Test + void shouldEncodeAndDecodeTemperature() { + assertEncodeDecode(StateCodec.Temperature8bitCodec.INSTANCE, new QuantityType<>(123, SIUnits.CELSIUS), + new byte[] { 123 }); + assertEncodeDecode(StateCodec.Temperature8bitCodec.INSTANCE, new QuantityType<>(1, SIUnits.CELSIUS), + new byte[] { 1 }); + assertEncodeDecode(StateCodec.Temperature8bitCodec.INSTANCE, new QuantityType<>(-1, SIUnits.CELSIUS), + new byte[] { b(255) }); + } +} diff --git a/bundles/pom.xml b/bundles/pom.xml index e43e5ec6fef..8ed05c5ad8c 100644 --- a/bundles/pom.xml +++ b/bundles/pom.xml @@ -110,6 +110,7 @@ org.openhab.binding.dwdpollenflug org.openhab.binding.dwdunwetter org.openhab.binding.easee + org.openhab.binding.echonetlite org.openhab.binding.ecobee org.openhab.binding.ecotouch org.openhab.binding.ecowatt