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;
52 public void applyProperty(InstanceKey sourceInstanceKey, Esv esv, final int epcCode, final int pdc,
53 final ByteBuffer edt) {
54 final Epc epc = Epc.lookup(instanceKey().klass.groupCode(), instanceKey().klass.classCode(), epcCode);
56 if ((Esv.Get_Res == esv || Esv.Get_SNA == esv || Esv.INF == esv) && 0 < pdc) {
57 pendingGets.remove(epc);
59 int edtPosition = edt.position();
61 final StateDecode decoder = epc.decoder();
63 if (null != decoder) {
64 state = decoder.decodeState(edt);
65 if (null == stateFields.put(epc, state)) {
66 epcByChannelId.put(epc.channelId(), epc);
69 final @Nullable State pendingState = lookupPendingSet(epc);
70 if (null != pendingState && pendingState.equals(state)) {
71 logger.debug("pendingSet - removing: {} {}", epc, state);
72 pendingSets.remove(epc);
73 } else if (null != pendingState) {
74 logger.debug("pendingSet - state mismatch: {} {} {}", epc, pendingState, state);
78 listener.onUpdated(epc.channelId(), state);
80 } else if (Epc.Device.GET_PROPERTY_MAP == epc) {
81 if (null == getPropertyMap) {
82 final EchonetPropertyMap getPropertyMap = new EchonetPropertyMap(epc);
83 getPropertyMap.update(edt);
84 getPropertyMap.getProperties(instanceKey().klass.groupCode(), instanceKey().klass.classCode(),
85 Set.of(Epc.Device.GET_PROPERTY_MAP), pendingGets);
86 this.getPropertyMap = getPropertyMap;
90 if (!initialised && null != getPropertyMap && pendingGets.isEmpty()) {
92 listener.onInitialised(identifier(), instanceKey, channelIds());
93 stateFields.forEach((e, s) -> listener.onUpdated(e.channelId(), s));
96 if (logger.isDebugEnabled()) {
97 String value = null != state ? state.toString() : "";
98 edt.position(edtPosition);
99 logger.debug("Applying: {}({},{}) {} {} pending: {}", epc, hex(epc.code()), pdc, value, hex(edt),
102 } else if (esv == Esv.Set_Res) {
103 pendingSets.remove(epc);
107 public String identifier() {
108 final State identificationNumber = stateFields.get(Epc.Device.IDENTIFICATION_NUMBER);
109 if (null == identificationNumber) {
110 throw new IllegalStateException("Echonet devices must support identification number property");
113 return identificationNumber.toString();
117 public boolean buildUpdateMessage(final EchonetMessageBuilder messageBuilder, final ShortSupplier tidSupplier,
118 final long nowMs, InstanceKey managementControllerKey) {
119 if (pendingSets.isEmpty()) {
123 final InflightRequest inflightSetRequest = this.inflightSetRequest;
125 if (hasInflight(nowMs, inflightSetRequest)) {
129 final short tid = tidSupplier.getAsShort();
130 messageBuilder.start(tid, managementControllerKey, instanceKey, Esv.SetC);
132 pendingSets.forEach((k, v) -> {
133 final StateEncode encoder = k.encoder();
134 if (null != encoder) {
135 final ByteBuffer buffer = messageBuilder.edtBuffer();
136 encoder.encodeState(v, buffer);
137 messageBuilder.appendEpcUpdate(k.code(), buffer.flip());
141 inflightSetRequest.requestSent(tid, nowMs);
147 public void update(String channelId, State state) {
148 final Epc epc = epcByChannelId.get(channelId);
150 logger.warn("Unable to find epc for channelId: {}", channelId);
154 pendingSets.put(epc, state);
158 public void removed() {
159 listener.onRemoved();
163 public void checkTimeouts() {
164 if (EchonetLiteBindingConstants.OFFLINE_TIMEOUT_COUNT <= inflightGetRequest.timeoutCount()) {
165 listener.onOffline();
170 public void refreshAll(long nowMs) {
171 final EchonetPropertyMap getPropertyMap = this.getPropertyMap;
172 if (lastPollMs + pollIntervalMs <= nowMs && null != getPropertyMap) {
173 getPropertyMap.getProperties(instanceKey().klass.groupCode(), instanceKey().klass.classCode(),
174 Set.of(Epc.Device.GET_PROPERTY_MAP), pendingGets);
180 public void refresh(String channelId) {
181 final Epc epc = epcByChannelId.get(channelId);
186 final State state = stateFields.get(epc);
191 listener.onUpdated(channelId, state);
194 public void setListener(EchonetDeviceListener listener) {
195 this.listener = listener;
197 listener.onInitialised(identifier(), instanceKey(), channelIds());
198 stateFields.forEach((e, s) -> listener.onUpdated(e.channelId(), s));
202 private Map<String, String> channelIds() {
203 final HashMap<String, String> channelIdAndType = new HashMap<>();
204 for (Epc e : stateFields.keySet()) {
205 final StateDecode decoder = e.decoder();
206 if (null != decoder) {
207 channelIdAndType.put(e.channelId(), decoder.itemType());
210 return channelIdAndType;
213 private @Nullable State lookupPendingSet(Epc epc) {
214 return pendingSets.get(epc);