2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.echonetlite.internal;
15 import static org.openhab.binding.echonetlite.internal.HexUtil.hex;
17 import java.nio.ByteBuffer;
18 import java.util.HashMap;
19 import java.util.LinkedHashMap;
23 import org.eclipse.jdt.annotation.NonNullByDefault;
24 import org.eclipse.jdt.annotation.Nullable;
25 import org.openhab.core.types.State;
26 import org.slf4j.Logger;
27 import org.slf4j.LoggerFactory;
30 * @author Michael Barker - Initial contribution
33 public class EchonetDevice extends EchonetObject {
35 private final LinkedHashMap<Epc, State> pendingSets = new LinkedHashMap<>();
36 private final HashMap<Epc, State> stateFields = new HashMap<>();
37 private final HashMap<String, Epc> epcByChannelId = new HashMap<>();
38 private final Logger logger = LoggerFactory.getLogger(EchonetDevice.class);
40 private EchonetPropertyMap getPropertyMap;
41 private EchonetDeviceListener listener;
42 private boolean initialised = false;
44 private long lastPollMs = 0;
46 public EchonetDevice(final InstanceKey instanceKey, EchonetDeviceListener listener) {
47 super(instanceKey, Epc.Device.GET_PROPERTY_MAP);
48 this.listener = listener;
51 public void applyProperty(InstanceKey sourceInstanceKey, Esv esv, final int epcCode, final int pdc,
52 final ByteBuffer edt) {
53 final Epc epc = Epc.lookup(instanceKey().klass.groupCode(), instanceKey().klass.classCode(), epcCode);
55 if ((Esv.Get_Res == esv || Esv.Get_SNA == esv || Esv.INF == esv) && 0 < pdc) {
56 pendingGets.remove(epc);
58 int edtPosition = edt.position();
60 final StateDecode decoder = epc.decoder();
62 if (null != decoder) {
63 state = decoder.decodeState(edt);
64 if (null == stateFields.put(epc, state)) {
65 epcByChannelId.put(epc.channelId(), epc);
68 final @Nullable State pendingState = lookupPendingSet(epc);
69 if (null != pendingState && pendingState.equals(state)) {
70 logger.debug("pendingSet - removing: {} {}", epc, state);
71 pendingSets.remove(epc);
72 } else if (null != pendingState) {
73 logger.debug("pendingSet - state mismatch: {} {} {}", epc, pendingState, state);
77 listener.onUpdated(epc.channelId(), state);
79 } else if (Epc.Device.GET_PROPERTY_MAP == epc) {
80 if (null == getPropertyMap) {
81 final EchonetPropertyMap getPropertyMap = new EchonetPropertyMap(epc);
82 getPropertyMap.update(edt);
83 getPropertyMap.getProperties(instanceKey().klass.groupCode(), instanceKey().klass.classCode(),
84 Set.of(Epc.Device.GET_PROPERTY_MAP), pendingGets);
85 this.getPropertyMap = getPropertyMap;
89 if (!initialised && null != getPropertyMap && pendingGets.isEmpty()) {
91 listener.onInitialised(identifier(), instanceKey, channelIds());
92 stateFields.forEach((e, s) -> listener.onUpdated(e.channelId(), s));
95 if (logger.isDebugEnabled()) {
96 String value = null != state ? state.toString() : "";
97 edt.position(edtPosition);
98 logger.debug("Applying: {}({},{}) {} {} pending: {}", epc, hex(epc.code()), pdc, value, hex(edt),
101 } else if (esv == Esv.Set_Res) {
102 pendingSets.remove(epc);
106 public String identifier() {
107 final State identificationNumber = stateFields.get(Epc.Device.IDENTIFICATION_NUMBER);
108 if (null == identificationNumber) {
109 throw new IllegalStateException("Echonet devices must support identification number property");
112 return identificationNumber.toString();
115 public boolean buildUpdateMessage(final EchonetMessageBuilder messageBuilder, final ShortSupplier tidSupplier,
116 final long nowMs, InstanceKey managementControllerKey) {
117 if (pendingSets.isEmpty()) {
121 final InflightRequest inflightSetRequest = this.inflightSetRequest;
123 if (hasInflight(nowMs, inflightSetRequest)) {
127 final short tid = tidSupplier.getAsShort();
128 messageBuilder.start(tid, managementControllerKey, instanceKey, Esv.SetC);
130 pendingSets.forEach((k, v) -> {
131 final StateEncode encoder = k.encoder();
132 if (null != encoder) {
133 final ByteBuffer buffer = messageBuilder.edtBuffer();
134 encoder.encodeState(v, buffer);
135 messageBuilder.appendEpcUpdate(k.code(), buffer.flip());
139 inflightSetRequest.requestSent(tid, nowMs);
144 public void update(String channelId, State state) {
145 final Epc epc = epcByChannelId.get(channelId);
147 logger.warn("Unable to find epc for channelId: {}", channelId);
151 pendingSets.put(epc, state);
155 public void removed() {
156 listener.onRemoved();
159 public void checkTimeouts() {
160 if (EchonetLiteBindingConstants.OFFLINE_TIMEOUT_COUNT <= inflightGetRequest.timeoutCount()) {
161 listener.onOffline();
165 public void refreshAll(long nowMs) {
166 final EchonetPropertyMap getPropertyMap = this.getPropertyMap;
167 if (lastPollMs + pollIntervalMs <= nowMs && null != getPropertyMap) {
168 getPropertyMap.getProperties(instanceKey().klass.groupCode(), instanceKey().klass.classCode(),
169 Set.of(Epc.Device.GET_PROPERTY_MAP), pendingGets);
175 public void refresh(String channelId) {
176 final Epc epc = epcByChannelId.get(channelId);
181 final State state = stateFields.get(epc);
186 listener.onUpdated(channelId, state);
189 public void setListener(EchonetDeviceListener listener) {
190 this.listener = listener;
192 listener.onInitialised(identifier(), instanceKey(), channelIds());
193 stateFields.forEach((e, s) -> listener.onUpdated(e.channelId(), s));
197 private Map<String, String> channelIds() {
198 final HashMap<String, String> channelIdAndType = new HashMap<>();
199 for (Epc e : stateFields.keySet()) {
200 final StateDecode decoder = e.decoder();
201 if (null != decoder) {
202 channelIdAndType.put(e.channelId(), decoder.itemType());
205 return channelIdAndType;
208 private @Nullable State lookupPendingSet(Epc epc) {
209 return pendingSets.get(epc);