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 java.util.Objects.requireNonNull;
17 import java.io.IOException;
18 import java.net.InetSocketAddress;
19 import java.net.SocketAddress;
20 import java.nio.ByteBuffer;
21 import java.util.Collection;
22 import java.util.HashMap;
23 import java.util.List;
25 import java.util.concurrent.ArrayBlockingQueue;
26 import java.util.concurrent.TimeUnit;
28 import org.eclipse.jdt.annotation.NonNullByDefault;
29 import org.eclipse.jdt.annotation.Nullable;
30 import org.openhab.core.thing.Bridge;
31 import org.openhab.core.thing.ChannelUID;
32 import org.openhab.core.thing.ThingStatus;
33 import org.openhab.core.thing.ThingStatusDetail;
34 import org.openhab.core.thing.binding.BaseBridgeHandler;
35 import org.openhab.core.thing.binding.ThingHandlerService;
36 import org.openhab.core.types.Command;
37 import org.openhab.core.types.State;
38 import org.slf4j.Logger;
39 import org.slf4j.LoggerFactory;
42 * Bridge handler for echonet lite devices. By default, all messages (inbound and outbound) happen on port 3610, so
43 * we can only have a single listener for echonet lite messages. Hence, using a bridge model to handle communications
46 * @author Michael Barker - Initial contribution
49 public class EchonetLiteBridgeHandler extends BaseBridgeHandler {
51 private final Logger logger = LoggerFactory.getLogger(EchonetLiteBridgeHandler.class);
52 private final ArrayBlockingQueue<Message> requests = new ArrayBlockingQueue<>(1024);
53 private final Map<InstanceKey, EchonetObject> devicesByKey = new HashMap<>();
54 private final EchonetMessageBuilder messageBuilder = new EchonetMessageBuilder();
55 private final Thread networkingThread = new Thread(this::poll);
56 private final EchonetMessage echonetMessage = new EchonetMessage();
57 private final MonotonicClock clock = new MonotonicClock();
60 private EchonetChannel echonetChannel;
63 private InstanceKey managementControllerKey;
66 private InstanceKey discoveryKey;
68 public EchonetLiteBridgeHandler(Bridge bridge) {
72 private void start(final InstanceKey managementControllerKey, InstanceKey discoveryKey) throws IOException {
73 this.managementControllerKey = managementControllerKey;
74 this.discoveryKey = discoveryKey;
76 logger.debug("Binding echonet channel");
77 echonetChannel = new EchonetChannel(discoveryKey.address);
78 logger.debug("Starting networking thread");
80 networkingThread.setName("OH-binding-" + EchonetLiteBindingConstants.BINDING_ID);
81 networkingThread.setDaemon(true);
82 networkingThread.start();
85 public void newDevice(InstanceKey instanceKey, long pollIntervalMs, long retryTimeoutMs,
86 final EchonetDeviceListener echonetDeviceListener) {
87 requests.add(new NewDeviceMessage(instanceKey, pollIntervalMs, retryTimeoutMs, echonetDeviceListener));
90 private void newDeviceInternal(final NewDeviceMessage message) {
91 final EchonetObject echonetObject = devicesByKey.get(message.instanceKey);
92 if (null != echonetObject) {
93 if (echonetObject instanceof EchonetDevice device) {
94 logger.debug("Update item: {} already discovered", message.instanceKey);
95 device.setTimeouts(message.pollIntervalMs, message.retryTimeoutMs);
96 device.setListener(message.echonetDeviceListener);
98 logger.debug("Item: {} already discovered, but was not a device", message.instanceKey);
101 logger.debug("New Device: {}", message.instanceKey);
102 final EchonetDevice device = new EchonetDevice(message.instanceKey, message.echonetDeviceListener);
103 device.setTimeouts(message.pollIntervalMs, message.retryTimeoutMs);
104 devicesByKey.put(message.instanceKey, device);
108 public void refreshDevice(final InstanceKey instanceKey, final String channelId) {
109 requests.add(new RefreshMessage(instanceKey, channelId));
112 private void refreshDeviceInternal(final RefreshMessage refreshMessage) {
113 final EchonetObject item = devicesByKey.get(refreshMessage.instanceKey);
115 item.refresh(refreshMessage.channelId);
119 public void removeDevice(final InstanceKey instanceKey) {
120 requests.add(new RemoveDevice(instanceKey));
123 private void removeDeviceInternal(final RemoveDevice removeDevice) {
124 final EchonetObject remove = devicesByKey.remove(removeDevice.instanceKey);
126 logger.debug("Removing device: {}, {}", removeDevice.instanceKey, remove);
127 if (null != remove) {
132 public void updateDevice(final InstanceKey instanceKey, final String id, final State command) {
133 requests.add(new UpdateDevice(instanceKey, id, command));
136 public void updateDeviceInternal(UpdateDevice updateDevice) {
137 final EchonetObject echonetObject = devicesByKey.get(updateDevice.instanceKey);
139 if (null == echonetObject) {
140 logger.warn("Device not found for update: {}", updateDevice);
144 echonetObject.update(updateDevice.channelId, updateDevice.state);
147 public void startDiscovery(EchonetDiscoveryListener echonetDiscoveryListener) {
148 requests.offer(new StartDiscoveryMessage(echonetDiscoveryListener, requireNonNull(discoveryKey)));
151 public void startDiscoveryInternal(StartDiscoveryMessage startDiscovery) {
152 devicesByKey.put(startDiscovery.instanceKey, new EchonetProfileNode(startDiscovery.instanceKey,
153 this::onDiscoveredInstanceKey, startDiscovery.echonetDiscoveryListener));
156 public void stopDiscovery() {
157 requests.offer(new StopDiscoveryMessage(requireNonNull(discoveryKey)));
160 private void stopDiscoveryInternal(StopDiscoveryMessage stopDiscovery) {
161 devicesByKey.remove(stopDiscovery.instanceKey);
164 private void onDiscoveredInstanceKey(EchonetDevice device) {
165 if (null == devicesByKey.putIfAbsent(device.instanceKey(), device)) {
166 logger.debug("New device discovered: {}", device.instanceKey);
170 private void pollDevices(long nowMs, EchonetChannel echonetChannel) {
171 for (EchonetObject echonetObject : devicesByKey.values()) {
172 if (echonetObject.buildUpdateMessage(messageBuilder, echonetChannel::nextTid, nowMs,
173 requireNonNull(managementControllerKey))) {
175 echonetChannel.sendMessage(messageBuilder);
176 } catch (IOException e) {
177 logger.warn("Failed to send echonet message", e);
181 echonetObject.refreshAll(nowMs);
183 if (echonetObject.buildPollMessage(messageBuilder, echonetChannel::nextTid, nowMs,
184 requireNonNull(managementControllerKey))) {
186 echonetChannel.sendMessage(messageBuilder);
187 } catch (IOException e) {
188 logger.warn("Failed to send echonet message", e);
191 echonetObject.checkTimeouts();
196 private void pollRequests() {
198 while (null != (message = requestsPoll())) {
199 logger.debug("Received request: {}", message);
200 if (message instanceof NewDeviceMessage deviceMessage) {
201 newDeviceInternal(deviceMessage);
202 } else if (message instanceof RefreshMessage refreshMessage) {
203 refreshDeviceInternal(refreshMessage);
204 } else if (message instanceof RemoveDevice device) {
205 removeDeviceInternal(device);
206 } else if (message instanceof UpdateDevice device) {
207 updateDeviceInternal(device);
208 } else if (message instanceof StartDiscoveryMessage discoveryMessage) {
209 startDiscoveryInternal(discoveryMessage);
210 } else if (message instanceof StopDiscoveryMessage discoveryMessage) {
211 stopDiscoveryInternal(discoveryMessage);
216 private @Nullable Message requestsPoll() {
217 return requests.poll();
220 private void pollNetwork(EchonetChannel echonetChannel) {
222 echonetChannel.pollMessages(echonetMessage, this::onMessage,
223 EchonetLiteBindingConstants.NETWORK_WAIT_TIMEOUT);
224 } catch (IOException e) {
225 logger.warn("Failed to poll for messages", e);
229 private void onMessage(final EchonetMessage echonetMessage, final SocketAddress sourceAddress) {
230 final EchonetClass echonetClass = echonetMessage.sourceClass();
231 if (null == echonetClass) {
232 logger.warn("Unable to find echonetClass for message: {}, from: {}", echonetMessage.toDebug(),
237 final InstanceKey instanceKey = new InstanceKey((InetSocketAddress) sourceAddress, echonetClass,
238 echonetMessage.instance());
239 final Esv esv = echonetMessage.esv();
241 EchonetObject echonetObject = devicesByKey.get(instanceKey);
242 if (null == echonetObject) {
243 echonetObject = devicesByKey.get(discoveryKey);
246 logger.debug("Message {} for: {}", esv, echonetObject);
247 if (null != echonetObject) {
248 echonetObject.applyHeader(esv, echonetMessage.tid(), clock.timeMs());
249 while (echonetMessage.moveNext()) {
250 final int epc = echonetMessage.currentEpc();
251 final int pdc = echonetMessage.currentPdc();
252 ByteBuffer edt = echonetMessage.currentEdt();
253 echonetObject.applyProperty(instanceKey, esv, epc, pdc, edt);
258 private void poll() {
261 updateStatus(ThingStatus.ONLINE);
263 while (!Thread.currentThread().isInterrupted()) {
266 } catch (Exception e) {
267 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
271 private void doPoll() {
272 final long nowMs = clock.timeMs();
274 pollDevices(nowMs, requireNonNull(echonetChannel));
275 pollNetwork(requireNonNull(echonetChannel));
279 public void initialize() {
280 final EchonetBridgeConfig bridgeConfig = getConfigAs(EchonetBridgeConfig.class);
282 final InstanceKey managementControllerKey = new InstanceKey(new InetSocketAddress(bridgeConfig.port),
283 EchonetClass.MANAGEMENT_CONTROLLER, (byte) 0x01);
284 final InstanceKey discoveryKey = new InstanceKey(
285 new InetSocketAddress(requireNonNull(bridgeConfig.multicastAddress), bridgeConfig.port),
286 EchonetClass.NODE_PROFILE, (byte) 0x01);
288 updateStatus(ThingStatus.UNKNOWN);
291 start(managementControllerKey, discoveryKey);
292 } catch (IOException e) {
293 throw new IllegalStateException("Unable to start networking thread", e);
298 public void dispose() {
299 if (networkingThread.isAlive()) {
300 networkingThread.interrupt();
302 networkingThread.join(TimeUnit.SECONDS.toMillis(5));
303 } catch (InterruptedException e) {
304 logger.debug("Interrupted while closing", e);
309 final EchonetChannel echonetChannel = this.echonetChannel;
310 if (null != echonetChannel) {
311 echonetChannel.close();
316 public void handleCommand(ChannelUID channelUID, Command command) {
320 public Collection<Class<? extends ThingHandlerService>> getServices() {
321 return List.of(EchonetDiscoveryService.class);
324 private abstract static class Message {
325 final InstanceKey instanceKey;
327 public Message(InstanceKey instanceKey) {
328 this.instanceKey = instanceKey;
332 private static final class NewDeviceMessage extends Message {
333 final long pollIntervalMs;
334 final long retryTimeoutMs;
335 final EchonetDeviceListener echonetDeviceListener;
337 public NewDeviceMessage(final InstanceKey instanceKey, long pollIntervalMs, long retryTimeoutMs,
338 final EchonetDeviceListener echonetDeviceListener) {
340 this.pollIntervalMs = pollIntervalMs;
341 this.retryTimeoutMs = retryTimeoutMs;
342 this.echonetDeviceListener = echonetDeviceListener;
346 public String toString() {
347 return "NewDeviceMessage{" + "instanceKey=" + instanceKey + ", pollIntervalMs=" + pollIntervalMs
348 + ", retryTimeoutMs=" + retryTimeoutMs + "} " + super.toString();
352 private static class RefreshMessage extends Message {
353 private final String channelId;
355 public RefreshMessage(InstanceKey instanceKey, String channelId) {
357 this.channelId = channelId;
361 private static class RemoveDevice extends Message {
362 public RemoveDevice(final InstanceKey instanceKey) {
367 private static class StartDiscoveryMessage extends Message {
368 private final EchonetDiscoveryListener echonetDiscoveryListener;
370 public StartDiscoveryMessage(EchonetDiscoveryListener echonetDiscoveryListener, InstanceKey discoveryKey) {
372 this.echonetDiscoveryListener = echonetDiscoveryListener;
376 private static class StopDiscoveryMessage extends Message {
377 public StopDiscoveryMessage(InstanceKey discoveryKey) {
382 private static class UpdateDevice extends Message {
383 private final String channelId;
384 private final State state;
386 public UpdateDevice(final InstanceKey instanceKey, final String channelId, final State state) {
388 this.channelId = channelId;
393 public String toString() {
394 return "UpdateDevice{" + "instanceKey=" + instanceKey + ", channelId='" + channelId + '\'' + ", state="
395 + state + "} " + super.toString();