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.Collections;
23 import java.util.HashMap;
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) {
94 logger.debug("Update item: {} already discovered", message.instanceKey);
95 EchonetDevice device = (EchonetDevice) echonetObject;
96 device.setTimeouts(message.pollIntervalMs, message.retryTimeoutMs);
97 device.setListener(message.echonetDeviceListener);
99 logger.debug("Item: {} already discovered, but was not a device", message.instanceKey);
102 logger.debug("New Device: {}", message.instanceKey);
103 final EchonetDevice device = new EchonetDevice(message.instanceKey, message.echonetDeviceListener);
104 device.setTimeouts(message.pollIntervalMs, message.retryTimeoutMs);
105 devicesByKey.put(message.instanceKey, device);
109 public void refreshDevice(final InstanceKey instanceKey, final String channelId) {
110 requests.add(new RefreshMessage(instanceKey, channelId));
113 private void refreshDeviceInternal(final RefreshMessage refreshMessage) {
114 final EchonetObject item = devicesByKey.get(refreshMessage.instanceKey);
116 item.refresh(refreshMessage.channelId);
120 public void removeDevice(final InstanceKey instanceKey) {
121 requests.add(new RemoveDevice(instanceKey));
124 private void removeDeviceInternal(final RemoveDevice removeDevice) {
125 final EchonetObject remove = devicesByKey.remove(removeDevice.instanceKey);
127 logger.debug("Removing device: {}, {}", removeDevice.instanceKey, remove);
128 if (null != remove) {
133 public void updateDevice(final InstanceKey instanceKey, final String id, final State command) {
134 requests.add(new UpdateDevice(instanceKey, id, command));
137 public void updateDeviceInternal(UpdateDevice updateDevice) {
138 final EchonetObject echonetObject = devicesByKey.get(updateDevice.instanceKey);
140 if (null == echonetObject) {
141 logger.warn("Device not found for update: {}", updateDevice);
145 echonetObject.update(updateDevice.channelId, updateDevice.state);
148 public void startDiscovery(EchonetDiscoveryListener echonetDiscoveryListener) {
149 requests.offer(new StartDiscoveryMessage(echonetDiscoveryListener, requireNonNull(discoveryKey)));
152 public void startDiscoveryInternal(StartDiscoveryMessage startDiscovery) {
153 devicesByKey.put(startDiscovery.instanceKey, new EchonetProfileNode(startDiscovery.instanceKey,
154 this::onDiscoveredInstanceKey, startDiscovery.echonetDiscoveryListener));
157 public void stopDiscovery() {
158 requests.offer(new StopDiscoveryMessage(requireNonNull(discoveryKey)));
161 private void stopDiscoveryInternal(StopDiscoveryMessage stopDiscovery) {
162 devicesByKey.remove(stopDiscovery.instanceKey);
165 private void onDiscoveredInstanceKey(EchonetDevice device) {
166 if (null == devicesByKey.putIfAbsent(device.instanceKey(), device)) {
167 logger.debug("New device discovered: {}", device.instanceKey);
171 private void pollDevices(long nowMs, EchonetChannel echonetChannel) {
172 for (EchonetObject echonetObject : devicesByKey.values()) {
173 if (echonetObject.buildUpdateMessage(messageBuilder, echonetChannel::nextTid, nowMs,
174 requireNonNull(managementControllerKey))) {
176 echonetChannel.sendMessage(messageBuilder);
177 } catch (IOException e) {
178 logger.warn("Failed to send echonet message", e);
182 echonetObject.refreshAll(nowMs);
184 if (echonetObject.buildPollMessage(messageBuilder, echonetChannel::nextTid, nowMs,
185 requireNonNull(managementControllerKey))) {
187 echonetChannel.sendMessage(messageBuilder);
188 } catch (IOException e) {
189 logger.warn("Failed to send echonet message", e);
192 echonetObject.checkTimeouts();
197 private void pollRequests() {
199 while (null != (message = requestsPoll())) {
200 logger.debug("Received request: {}", message);
201 if (message instanceof NewDeviceMessage) {
202 newDeviceInternal((NewDeviceMessage) message);
203 } else if (message instanceof RefreshMessage) {
204 refreshDeviceInternal((RefreshMessage) message);
205 } else if (message instanceof RemoveDevice) {
206 removeDeviceInternal((RemoveDevice) message);
207 } else if (message instanceof UpdateDevice) {
208 updateDeviceInternal((UpdateDevice) message);
209 } else if (message instanceof StartDiscoveryMessage) {
210 startDiscoveryInternal((StartDiscoveryMessage) message);
211 } else if (message instanceof StopDiscoveryMessage) {
212 stopDiscoveryInternal((StopDiscoveryMessage) message);
217 private @Nullable Message requestsPoll() {
218 return requests.poll();
221 private void pollNetwork(EchonetChannel echonetChannel) {
223 echonetChannel.pollMessages(echonetMessage, this::onMessage,
224 EchonetLiteBindingConstants.NETWORK_WAIT_TIMEOUT);
225 } catch (IOException e) {
226 logger.warn("Failed to poll for messages", e);
230 private void onMessage(final EchonetMessage echonetMessage, final SocketAddress sourceAddress) {
231 final EchonetClass echonetClass = echonetMessage.sourceClass();
232 if (null == echonetClass) {
233 logger.warn("Unable to find echonetClass for message: {}, from: {}", echonetMessage.toDebug(),
238 final InstanceKey instanceKey = new InstanceKey((InetSocketAddress) sourceAddress, echonetClass,
239 echonetMessage.instance());
240 final Esv esv = echonetMessage.esv();
242 EchonetObject echonetObject = devicesByKey.get(instanceKey);
243 if (null == echonetObject) {
244 echonetObject = devicesByKey.get(discoveryKey);
247 logger.debug("Message {} for: {}", esv, echonetObject);
248 if (null != echonetObject) {
249 echonetObject.applyHeader(esv, echonetMessage.tid(), clock.timeMs());
250 while (echonetMessage.moveNext()) {
251 final int epc = echonetMessage.currentEpc();
252 final int pdc = echonetMessage.currentPdc();
253 ByteBuffer edt = echonetMessage.currentEdt();
254 echonetObject.applyProperty(instanceKey, esv, epc, pdc, edt);
259 private void poll() {
262 updateStatus(ThingStatus.ONLINE);
264 while (!Thread.currentThread().isInterrupted()) {
267 } catch (Exception e) {
268 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
272 private void doPoll() {
273 final long nowMs = clock.timeMs();
275 pollDevices(nowMs, requireNonNull(echonetChannel));
276 pollNetwork(requireNonNull(echonetChannel));
280 public void initialize() {
281 final EchonetBridgeConfig bridgeConfig = getConfigAs(EchonetBridgeConfig.class);
283 final InstanceKey managementControllerKey = new InstanceKey(new InetSocketAddress(bridgeConfig.port),
284 EchonetClass.MANAGEMENT_CONTROLLER, (byte) 0x01);
285 final InstanceKey discoveryKey = new InstanceKey(
286 new InetSocketAddress(requireNonNull(bridgeConfig.multicastAddress), bridgeConfig.port),
287 EchonetClass.NODE_PROFILE, (byte) 0x01);
289 updateStatus(ThingStatus.UNKNOWN);
292 start(managementControllerKey, discoveryKey);
293 } catch (IOException e) {
294 throw new IllegalStateException("Unable to start networking thread", e);
299 public void dispose() {
300 if (networkingThread.isAlive()) {
301 networkingThread.interrupt();
303 networkingThread.join(TimeUnit.SECONDS.toMillis(5));
304 } catch (InterruptedException e) {
305 logger.debug("Interrupted while closing", e);
310 final EchonetChannel echonetChannel = this.echonetChannel;
311 if (null != echonetChannel) {
312 echonetChannel.close();
317 public void handleCommand(ChannelUID channelUID, Command command) {
321 public Collection<Class<? extends ThingHandlerService>> getServices() {
322 return Collections.singletonList(EchonetDiscoveryService.class);
325 private abstract static class Message {
326 final InstanceKey instanceKey;
328 public Message(InstanceKey instanceKey) {
329 this.instanceKey = instanceKey;
333 private static final class NewDeviceMessage extends Message {
334 final long pollIntervalMs;
335 final long retryTimeoutMs;
336 final EchonetDeviceListener echonetDeviceListener;
338 public NewDeviceMessage(final InstanceKey instanceKey, long pollIntervalMs, long retryTimeoutMs,
339 final EchonetDeviceListener echonetDeviceListener) {
341 this.pollIntervalMs = pollIntervalMs;
342 this.retryTimeoutMs = retryTimeoutMs;
343 this.echonetDeviceListener = echonetDeviceListener;
347 public String toString() {
348 return "NewDeviceMessage{" + "instanceKey=" + instanceKey + ", pollIntervalMs=" + pollIntervalMs
349 + ", retryTimeoutMs=" + retryTimeoutMs + "} " + super.toString();
353 private static class RefreshMessage extends Message {
354 private final String channelId;
356 public RefreshMessage(InstanceKey instanceKey, String channelId) {
358 this.channelId = channelId;
362 private static class RemoveDevice extends Message {
363 public RemoveDevice(final InstanceKey instanceKey) {
368 private static class StartDiscoveryMessage extends Message {
369 private final EchonetDiscoveryListener echonetDiscoveryListener;
371 public StartDiscoveryMessage(EchonetDiscoveryListener echonetDiscoveryListener, InstanceKey discoveryKey) {
373 this.echonetDiscoveryListener = echonetDiscoveryListener;
377 private static class StopDiscoveryMessage extends Message {
378 public StopDiscoveryMessage(InstanceKey discoveryKey) {
383 private static class UpdateDevice extends Message {
384 private final String channelId;
385 private final State state;
387 public UpdateDevice(final InstanceKey instanceKey, final String channelId, final State state) {
389 this.channelId = channelId;
393 public String toString() {
394 return "UpdateDevice{" + "instanceKey=" + instanceKey + ", channelId='" + channelId + '\'' + ", state="
395 + state + "} " + super.toString();