[echonetlite] Initial contribution (#11999)

* First implementation of Echonet Lite Java Bindings.  Only supports Mitsubishi Home Heat Pumps.

Signed-off-by: Michael Barker <mikeb01@gmail.com>
This commit is contained in:
Michael Barker 2022-09-29 10:23:57 +13:00 committed by GitHub
parent f74f3ecd02
commit c244391d08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
43 changed files with 4197 additions and 0 deletions

View File

@ -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

View File

@ -381,6 +381,11 @@
<artifactId>org.openhab.binding.easee</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.echonetlite</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.binding.ecobee</artifactId>

View File

@ -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

View File

@ -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"}
```

View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.openhab.addons.bundles</groupId>
<artifactId>org.openhab.addons.reactor.bundles</artifactId>
<version>3.4.0-SNAPSHOT</version>
</parent>
<artifactId>org.openhab.binding.echonetlite</artifactId>
<name>openHAB Add-ons :: Bundles :: EchonetLite Binding</name>
</project>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<features name="org.openhab.binding.echonetlite-${project.version}" xmlns="http://karaf.apache.org/xmlns/features/v1.4.0">
<repository>mvn:org.openhab.core.features.karaf/org.openhab.core.features.karaf.openhab-core/${ohc.version}/xml/features</repository>
<feature name="openhab-binding-echonetlite" description="EchonetLite Binding" version="${project.version}">
<feature>openhab-runtime-base</feature>
<bundle start-level="80">mvn:org.openhab.addons.bundles/org.openhab.binding.echonetlite/${project.version}</bundle>
</feature>
</features>

View File

@ -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 + '}';
}
}

View File

@ -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<NetworkInterface> 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<EchonetMessage, SocketAddress> 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);
}
}

View File

@ -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) + '}';
}
}

View File

@ -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)];
}
}

View File

@ -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<Epc, State> pendingSets = new LinkedHashMap<>();
private final HashMap<Epc, State> stateFields = new HashMap<>();
private final HashMap<String, Epc> 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<String, String> channelIds() {
final HashMap<String, String> 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);
}
}

View File

@ -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 + '}';
}
}

View File

@ -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<String, String> channelIdAndType) {
}
default void onUpdated(String channelId, State value) {
}
default void onRemoved() {
}
default void onOffline() {
}
}

View File

@ -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);
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -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<Message> requests = new ArrayBlockingQueue<>(1024);
private final Map<InstanceKey, EchonetObject> 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<Class<? extends ThingHandlerService>> 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();
}
}
}

View File

@ -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<String, State> 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<String, String> channelIdAndType) {
logger.debug("Initialised Channels: {}", channelIdAndType);
final List<String> 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);
}
}
}

View File

@ -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<ThingTypeUID> 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;
}
}

View File

@ -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();
}
}

View File

@ -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();
}
}

View File

@ -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<Epc> 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<Short, Long> 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;
}
}
}

View File

@ -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<EchonetDevice> newDeviceListener;
private final EchonetDiscoveryListener echonetDiscoveryListener;
private long lastPollMs = 0;
public EchonetProfileNode(final InstanceKey instanceKey, Consumer<EchonetDevice> 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<String, String> channelIdAndType) {
echonetDiscoveryListener.onDeviceFound(identifier, instanceKey);
}
}

View File

@ -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<Epc> existing, Collection<Epc> 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) + '}';
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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<String, Option> 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()));
}
}
}

View File

@ -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();
}

View File

@ -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);
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<binding:binding id="echonetlite" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:binding="https://openhab.org/schemas/binding/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/binding/v1.0.0 https://openhab.org/schemas/binding-1.0.0.xsd">
<name>EchonetLite Binding</name>
<description>This is the binding for EchonetLite.</description>
</binding:binding>

View File

@ -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

View File

@ -0,0 +1,378 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="echonetlite"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<channel-type id="operationStatus">
<item-type>Switch</item-type>
<label>Operation Status</label>
<description>Operation Status</description>
<category>Switch</category>
</channel-type>
<channel-type id="installationLocation">
<item-type>String</item-type>
<label>Installation Location</label>
<description>Installation Location</description>
<category>Text</category>
<state>
<options>
<option value="Not specified">Not specified</option>
<option value="Living Room">Living Room</option>
<option value="Living Room 1">Living Room 1</option>
<option value="Living Room 2">Living Room 2</option>
<option value="Living Room 3">Living Room 3</option>
<option value="Living Room 4">Living Room 4</option>
<option value="Living Room 5">Living Room 5</option>
<option value="Living Room 6">Living Room 6</option>
<option value="Living Room 7">Living Room 7</option>
<option value="Dining Room">Dining Room</option>
<option value="Dining Room 1">Dining Room 1</option>
<option value="Dining Room 2">Dining Room 2</option>
<option value="Dining Room 3">Dining Room 3</option>
<option value="Dining Room 4">Dining Room 4</option>
<option value="Dining Room 5">Dining Room 5</option>
<option value="Dining Room 6">Dining Room 6</option>
<option value="Dining Room 7">Dining Room 7</option>
<option value="Kitchen">"Kitchen"</option>
<option value="Kitchen 1">Kitchen 1</option>
<option value="Kitchen 2">Kitchen 2</option>
<option value="Kitchen 3">Kitchen 3</option>
<option value="Kitchen 4">Kitchen 4</option>
<option value="Kitchen 5">Kitchen 5</option>
<option value="Kitchen 6">Kitchen 6</option>
<option value="Kitchen 7">Kitchen 7</option>
<option value="Lavatory">"Lavatory"</option>
<option value="Lavatory 1">Lavatory 1</option>
<option value="Lavatory 2">Lavatory 2</option>
<option value="Lavatory 3">Lavatory 3</option>
<option value="Lavatory 4">Lavatory 4</option>
<option value="Lavatory 5">Lavatory 5</option>
<option value="Lavatory 6">Lavatory 6</option>
<option value="Lavatory 7">Lavatory 7</option>
<option value="Washroom/changing room">Washroom/changing room</option>
<option value="Washroom/changing room 1">Washroom/changing room 1</option>
<option value="Washroom/changing room 2">Washroom/changing room 2</option>
<option value="Washroom/changing room 3">Washroom/changing room 3</option>
<option value="Washroom/changing room 4">Washroom/changing room 4</option>
<option value="Washroom/changing room 5">Washroom/changing room 5</option>
<option value="Washroom/changing room 6">Washroom/changing room 6</option>
<option value="Washroom/changing room 7">Washroom/changing room 7</option>
<option value="Passageway">"Passageway"</option>
<option value="Passageway 1">Passageway 1</option>
<option value="Passageway 2">Passageway 2</option>
<option value="Passageway 3">Passageway 3</option>
<option value="Passageway 4">Passageway 4</option>
<option value="Passageway 5">Passageway 5</option>
<option value="Passageway 6">Passageway 6</option>
<option value="Passageway 7">Passageway 7</option>
<option value="Room">"Room"</option>
<option value="Room 1">Room 1</option>
<option value="Room 2">Room 2</option>
<option value="Room 3">Room 3</option>
<option value="Room 4">Room 4</option>
<option value="Room 5">Room 5</option>
<option value="Room 6">Room 6</option>
<option value="Room 7">Room 7</option>
<option value="Stairway">"Stairway"</option>
<option value="Stairway 1">Stairway 1</option>
<option value="Stairway 2">Stairway 2</option>
<option value="Stairway 3">Stairway 3</option>
<option value="Stairway 4">Stairway 4</option>
<option value="Stairway 5">Stairway 5</option>
<option value="Stairway 6">Stairway 6</option>
<option value="Stairway 7">Stairway 7</option>
<option value="Front door">Front door</option>
<option value="Front door 1">Front door 1</option>
<option value="Front door 2">Front door 2</option>
<option value="Front door 3">Front door 3</option>
<option value="Front door 4">Front door 4</option>
<option value="Front door 5">Front door 5</option>
<option value="Front door 6">Front door 6</option>
<option value="Front door 7">Front door 7</option>
<option value="Storeroom">"Storeroom"</option>
<option value="Storeroom 1">Storeroom 1</option>
<option value="Storeroom 2">Storeroom 2</option>
<option value="Storeroom 3">Storeroom 3</option>
<option value="Storeroom 4">Storeroom 4</option>
<option value="Storeroom 5">Storeroom 5</option>
<option value="Storeroom 6">Storeroom 6</option>
<option value="Storeroom 7">Storeroom 7</option>
<option value="Garden/perimeter">Garden/perimeter</option>
<option value="Garden/perimeter 1">Garden/perimeter 1</option>
<option value="Garden/perimeter 2">Garden/perimeter 2</option>
<option value="Garden/perimeter 3">Garden/perimeter 3</option>
<option value="Garden/perimeter 4">Garden/perimeter 4</option>
<option value="Garden/perimeter 5">Garden/perimeter 5</option>
<option value="Garden/perimeter 6">Garden/perimeter 6</option>
<option value="Garden/perimeter 7">Garden/perimeter 7</option>
<option value="Garage">"Garage"</option>
<option value="Garage 1">Garage 1</option>
<option value="Garage 2">Garage 2</option>
<option value="Garage 3">Garage 3</option>
<option value="Garage 4">Garage 4</option>
<option value="Garage 5">Garage 5</option>
<option value="Garage 6">Garage 6</option>
<option value="Garage 7">Garage 7</option>
<option value="Veranda/balcony">Veranda/balcony</option>
<option value="Veranda/balcony 1">Veranda/balcony 1</option>
<option value="Veranda/balcony 2">Veranda/balcony 2</option>
<option value="Veranda/balcony 3">Veranda/balcony 3</option>
<option value="Veranda/balcony 4">Veranda/balcony 4</option>
<option value="Veranda/balcony 5">Veranda/balcony 5</option>
<option value="Veranda/balcony 6">Veranda/balcony 6</option>
<option value="Veranda/balcony 7">Veranda/balcony 7</option>
<option value="Others">"Others"</option>
<option value="Others 1">Others 1</option>
<option value="Others 2">Others 2</option>
<option value="Others 3">Others 3</option>
<option value="Others 4">Others 4</option>
<option value="Others 5">Others 5</option>
<option value="Others 6">Others 6</option>
<option value="Others 7">Others 7</option>
</options>
</state>
</channel-type>
<channel-type id="standardVersionInformation">
<item-type>String</item-type>
<label>Standard Version Information</label>
<description>Standard Version Information</description>
<category>Text</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="identificationNumber">
<item-type>String</item-type>
<label>Identification Number</label>
<description>Identification Number</description>
<category>Text</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="manufacturerFaultCode">
<item-type>String</item-type>
<label>Manufacturer Fault Code</label>
<description>Manufacturer Fault Code</description>
<category>Text</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="faultStatus">
<item-type>Switch</item-type>
<label>Fault Status</label>
<description>Fault Status</description>
<category>Alarm</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="faultDescription">
<item-type>String</item-type>
<label>Fault Description</label>
<description>Fault Description</description>
<category>Text</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="manufacturerCode">
<item-type>String</item-type>
<label>Manufacturer Code</label>
<description>Manufacturer Code</description>
<category>Text</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="businessFacilityCode">
<item-type>String</item-type>
<label>Business Facility Code</label>
<description>Business Facility Code</description>
<category>Text</category>
<state readOnly="true"/>
</channel-type>
<channel-type id="powerSavingOperationSetting">
<item-type>Switch</item-type>
<label>Power Saving</label>
<description>Controls whether the unit is in power saving operation or not</description>
<category>Switch</category>
</channel-type>
<channel-type id="cumulativeOperatingTime">
<item-type>Number:Time</item-type>
<label>Cumulative Operating Time</label>
<description>Cumulative time the unit has been operating in seconds</description>
<category>Time</category>
</channel-type>
<channel-type id="airFlowRate">
<item-type>String</item-type>
<label>Air Flow Rate</label>
<description>Air Flow Rate</description>
<category>Flow</category>
<state>
<options>
<option value="Auto">Auto</option>
<option value="Rate 1">Rate 1</option>
<option value="Rate 2">Rate 2</option>
<option value="Rate 3">Rate 3</option>
<option value="Rate 4">Rate 4</option>
<option value="Rate 5">Rate 5</option>
<option value="Rate 6">Rate 6</option>
<option value="Rate 7">Rate 7</option>
<option value="Rate 8">Rate 8</option>
</options>
</state>
</channel-type>
<channel-type id="automaticControlOfAirFlowDirection">
<item-type>String</item-type>
<label>Automatic Air Flow Direction</label>
<description>The type of automatic control applied to the air flow direction, if any</description>
<state>
<options>
<option value="Automatic">Automatic</option>
<option value="Non-automatic">Non-automatic</option>
<option value="Automatic (vertical)">Automatic (vertical)</option>
<option value="Automatic (horizontal)">Automatic (horizontal)</option>
</options>
</state>
</channel-type>
<channel-type id="automaticSwingOfAirFlow">
<item-type>String</item-type>
<label>Automatic Swing Of Air Flow</label>
<description>Automatic Swing Of Air Flow</description>
<state>
<options>
<option value="Not used">Not used</option>
<option value="Used (vertical)">Used (vertical)</option>
<option value="Used (horizontal)">Used (horizontal)</option>
<option value="Used (vertical and horizontal)">Used (vertical and horizontal)</option>
</options>
</state>
</channel-type>
<channel-type id="airFlowDirectionVertical">
<item-type>String</item-type>
<label>Air Flow Direction Vertical</label>
<description>Air Flow Direction Vertical</description>
<state>
<options>
<option value="Uppermost">Uppermost</option>
<option value="Lowermost">Lowermost</option>
<option value="Mid-uppermost">Mid-uppermost</option>
<option value="Mid-lowermost">Mid-lowermost</option>
<option value="Central">Central</option>
</options>
</state>
</channel-type>
<channel-type id="airFlowDirectionHorizontal">
<item-type>String</item-type>
<label>Air Flow Direction Horizontal</label>
<description>Air Flow Direction Horizontal</description>
<state>
<options>
<option value="XXXOO">XXXOO</option>
<option value="OOXXX">OOXXX</option>
<option value="XOOOX">XOOOX</option>
<option value="OOXOO">OOXOO</option>
<option value="XXXXO">XXXXO</option>
<option value="XXXOX">XXXOX</option>
<option value="XXOXX">XXOXX</option>
<option value="XXOXO">XXOXO</option>
<option value="XXOOX">XXOOX</option>
<option value="XXOOO">XXOOO</option>
<option value="XOXXX">XOXXX</option>
<option value="XOXXO">XOXXO</option>
<option value="XOXOX">XOXOX</option>
<option value="XOXOO">XOXOO</option>
<option value="XOOXX">XOOXX</option>
<option value="XOOXO">XOOXO</option>
<option value="XOOOO">XOOOO</option>
<option value="OXXXX">OXXXX</option>
<option value="OXXXO">OXXXO</option>
<option value="OXXOX">OXXOX</option>
<option value="OXXOO">OXXOO</option>
<option value="OXOXX">OXOXX</option>
<option value="OXOXO">OXOXO</option>
<option value="OXOOX">OXOOX</option>
<option value="OXOOO">OXOOO</option>
<option value="OOXXO">OOXXO</option>
<option value="OOXOX">OOXOX</option>
<option value="OOOXX">OOOXX</option>
<option value="OOOXO">OOOXO</option>
<option value="OOOOX">OOOOX</option>
<option value="OOOOO">OOOOO</option>
</options>
</state>
</channel-type>
<channel-type id="operationMode">
<item-type>String</item-type>
<label>Operation Mode</label>
<description>The current mode for the Home AC unit (heating, cooling, etc.)</description>
<state>
<options>
<option value="Automatic">Automatic</option>
<option value="Cooling">Cooling</option>
<option value="Heating">Heating</option>
<option value="Dry">Dry</option>
<option value="Fan">Fan</option>
<option value="Other">Other</option>
</options>
</state>
</channel-type>
<channel-type id="setTemperature">
<item-type>Number:Temperature</item-type>
<label>Set Temperature</label>
<description>Desired target room temperature</description>
<category>Temperature</category>
<tags>
<tag>Setpoint</tag>
<tag>Temperature</tag>
</tags>
<state min="0" max="50" pattern="%d %unit%" readOnly="false"/>
</channel-type>
<channel-type id="measuredRoomTemperature">
<item-type>Number:Temperature</item-type>
<label>Measured Room Temperature</label>
<description>Measured Room Temperature</description>
<category>Temperature</category>
<tags>
<tag>Measurement</tag>
<tag>Temperature</tag>
</tags>
<state min="-127" max="125" pattern="%d %unit%" readOnly="true"/>
</channel-type>
<channel-type id="measuredOutdoorTemperature">
<item-type>Number:Temperature</item-type>
<label>Measured Outdoor Temperature</label>
<description>Measured Outdoor Temperature</description>
<category>Temperature</category>
<tags>
<tag>Measurement</tag>
<tag>Temperature</tag>
</tags>
<state min="-127" max="125" pattern="%d %unit%" readOnly="true"/>
</channel-type>
</thing:thing-descriptions>

View File

@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<thing:thing-descriptions bindingId="echonetlite"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:thing="https://openhab.org/schemas/thing-description/v1.0.0"
xsi:schemaLocation="https://openhab.org/schemas/thing-description/v1.0.0 https://openhab.org/schemas/thing-description-1.0.0.xsd">
<bridge-type id="bridge">
<label>Echonet Bridge</label>
<description>Virtual bridge to ensure that there is only a single binding to the echonet port</description>
<representation-property>port</representation-property>
<config-description>
<parameter name="multicastAddress" type="text" required="true">
<context>network-address</context>
<label>Discovery/Notification Address</label>
<description>Address used to discover nodes and receive notifications</description>
<default>224.0.23.0</default>
</parameter>
<parameter name="port" type="integer" required="true">
<label>Echonet Port</label>
<description>Port used for echonet messages both outbound and inbound</description>
<default>3610</default>
</parameter>
</config-description>
</bridge-type>
<thing-type id="device">
<supported-bridge-type-refs>
<bridge-type-ref id="bridge"/>
</supported-bridge-type-refs>
<label>EchonetLite Device</label>
<description>Device for EchonetLite Binding</description>
<representation-property>instanceKey</representation-property>
<config-description>
<parameter name="hostname" type="text" required="true">
<context>network-address</context>
<label>Hostname</label>
<description>Hostname or IP address of the device</description>
</parameter>
<parameter name="port" type="integer" required="true">
<default>3610</default>
<label>Port</label>
<description>Port of the device (usually 3610)</description>
</parameter>
<parameter name="groupCode" type="integer" required="true">
<label>Group Code</label>
<description>Echonet Group Code</description>
</parameter>
<parameter name="classCode" type="integer" required="true">
<label>Class Code</label>
<description>Echonet Class Code</description>
</parameter>
<parameter name="instance" type="integer" required="true">
<label>Instance</label>
<description>Echonet Instance</description>
</parameter>
<parameter name="pollIntervalMs" type="integer" required="true">
<default>30000</default>
<label>Poll Interval (ms)</label>
<description>Interval in ms between each poll of the device</description>
</parameter>
<parameter name="retryTimeoutMs" type="integer" required="true">
<default>2000</default>
<label>Retry Timeout (ms)</label>
<description>Timeout in ms before a message is resent</description>
</parameter>
</config-description>
</thing-type>
</thing:thing-descriptions>

View File

@ -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()));
}
}
}

View File

@ -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"));
}
}

View File

@ -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) });
}
}

View File

@ -110,6 +110,7 @@
<module>org.openhab.binding.dwdpollenflug</module>
<module>org.openhab.binding.dwdunwetter</module>
<module>org.openhab.binding.easee</module>
<module>org.openhab.binding.echonetlite</module>
<module>org.openhab.binding.ecobee</module>
<module>org.openhab.binding.ecotouch</module>
<module>org.openhab.binding.ecowatt</module>