diff --git a/CODEOWNERS b/CODEOWNERS
index 32edcbe2c..cf6bc84ef 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 73ebdac24..be5f6fa78 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 000000000..38d625e34
--- /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 000000000..fd4da57d6
--- /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 000000000..20a4db3e9
--- /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 000000000..951022092
--- /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 000000000..ad916b7cd
--- /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 000000000..51d08c41c
--- /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 000000000..5a4e4dd3c
--- /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 000000000..95c48f66d
--- /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 000000000..7dcf22ec6
--- /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 000000000..2657ff402
--- /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 000000000..1d6fb4302
--- /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 000000000..3769bbe25
--- /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 000000000..a7692f1f2
--- /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 000000000..9060600a2
--- /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 000000000..4e6f03639
--- /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 000000000..87297e04d
--- /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 000000000..158dd29b7
--- /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 000000000..061fb896b
--- /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 000000000..2282eca29
--- /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 000000000..28d918e74
--- /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 000000000..af4046acc
--- /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 000000000..40d1caf61
--- /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 000000000..b09b82364
--- /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 000000000..37c9009db
--- /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 000000000..3f22002bf
--- /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 000000000..e215a358e
--- /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 000000000..b99463625
--- /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 000000000..60c7184ba
--- /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 000000000..f807a1a3c
--- /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 000000000..d2e8dbf2c
--- /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 000000000..b4adf8409
--- /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 000000000..580c810ca
--- /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 000000000..94f15f9de
--- /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 000000000..c69e0af6e
--- /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 000000000..c1f4762b7
--- /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 000000000..84da5eeaa
--- /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 000000000..3b90dea08
--- /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 000000000..c6cf26e2d
--- /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 000000000..a0b8f08c5
--- /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 000000000..7d070f877
--- /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 e43e5ec6f..8ed05c5ad 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