2 * Copyright (c) 2010-2024 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.network.internal.handler;
15 import static org.openhab.binding.network.internal.NetworkBindingConstants.*;
16 import static org.openhab.binding.network.internal.utils.NetworkUtils.durationToMillis;
18 import java.time.Duration;
19 import java.time.Instant;
20 import java.time.ZonedDateTime;
21 import java.util.Collection;
22 import java.util.List;
25 import java.util.TimeZone;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.openhab.binding.network.internal.NetworkBindingConfiguration;
29 import org.openhab.binding.network.internal.NetworkBindingConfigurationListener;
30 import org.openhab.binding.network.internal.NetworkBindingConstants;
31 import org.openhab.binding.network.internal.NetworkHandlerConfiguration;
32 import org.openhab.binding.network.internal.PresenceDetection;
33 import org.openhab.binding.network.internal.PresenceDetectionListener;
34 import org.openhab.binding.network.internal.PresenceDetectionValue;
35 import org.openhab.binding.network.internal.WakeOnLanPacketSender;
36 import org.openhab.binding.network.internal.action.NetworkActions;
37 import org.openhab.core.library.types.DateTimeType;
38 import org.openhab.core.library.types.OnOffType;
39 import org.openhab.core.library.types.QuantityType;
40 import org.openhab.core.library.unit.MetricPrefix;
41 import org.openhab.core.library.unit.Units;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.binding.BaseThingHandler;
47 import org.openhab.core.thing.binding.ThingHandlerService;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.types.RefreshType;
50 import org.openhab.core.types.UnDefType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
55 * This handler is handling the CHANNEL_ONLINE (boolean) and CHANNEL_TIME (time in ms)
56 * commands and is starting a {@see NetworkService} instance for the configured device.
58 * @author Marc Mettke - Initial contribution
59 * @author David Graeff - Rewritten
60 * @author Wouter Born - Add Wake-on-LAN thing action support
63 public class NetworkHandler extends BaseThingHandler
64 implements PresenceDetectionListener, NetworkBindingConfigurationListener {
65 private final Logger logger = LoggerFactory.getLogger(NetworkHandler.class);
66 private @NonNullByDefault({}) PresenceDetection presenceDetection;
67 private @NonNullByDefault({}) WakeOnLanPacketSender wakeOnLanPacketSender;
69 private boolean isTCPServiceDevice;
70 private NetworkBindingConfiguration configuration;
72 // How many retries before a device is deemed offline
74 // Retry counter. Will be reset as soon as a device presence detection succeed.
75 private int retryCounter = 0;
76 private NetworkHandlerConfiguration handlerConfiguration = new NetworkHandlerConfiguration();
79 * Do not call this directly, but use the {@see NetworkHandlerBuilder} instead.
81 public NetworkHandler(Thing thing, boolean isTCPServiceDevice, NetworkBindingConfiguration configuration) {
83 this.isTCPServiceDevice = isTCPServiceDevice;
84 this.configuration = configuration;
85 this.configuration.addNetworkBindingConfigurationListener(this);
88 private void refreshValue(ChannelUID channelUID) {
89 // We are not yet even initialized, don't do anything
90 if (presenceDetection == null || !presenceDetection.isAutomaticRefreshing()) {
94 switch (channelUID.getId()) {
96 presenceDetection.getValue(value -> updateState(CHANNEL_ONLINE, OnOffType.from(value.isReachable())));
99 presenceDetection.getValue(value -> {
100 double latencyMs = durationToMillis(value.getLowestLatency());
101 updateState(CHANNEL_LATENCY, new QuantityType<>(latencyMs, MetricPrefix.MILLI(Units.SECOND)));
104 case CHANNEL_LASTSEEN:
105 Instant lastSeen = presenceDetection.getLastSeen();
106 if (lastSeen != null) {
107 updateState(CHANNEL_LASTSEEN, new DateTimeType(
108 ZonedDateTime.ofInstant(lastSeen, TimeZone.getDefault().toZoneId()).withFixedOffsetZone()));
110 updateState(CHANNEL_LASTSEEN, UnDefType.UNDEF);
114 logger.debug("Command received for an unknown channel: {}", channelUID.getId());
120 public void handleCommand(ChannelUID channelUID, Command command) {
121 if (command instanceof RefreshType) {
122 refreshValue(channelUID);
124 logger.debug("Command {} is not supported for channel: {}", command, channelUID.getId());
129 public void partialDetectionResult(PresenceDetectionValue value) {
130 double latencyMs = durationToMillis(value.getLowestLatency());
131 updateState(CHANNEL_ONLINE, OnOffType.ON);
132 updateState(CHANNEL_LATENCY, new QuantityType<>(latencyMs, MetricPrefix.MILLI(Units.SECOND)));
136 public void finalDetectionResult(PresenceDetectionValue value) {
137 // We do not notify the framework immediately if a device presence detection failed and
138 // the user configured retries to be > 1.
139 retryCounter = value.isReachable() ? 0 : retryCounter + 1;
141 if (retryCounter >= retries) {
142 updateState(CHANNEL_ONLINE, OnOffType.OFF);
143 updateState(CHANNEL_LATENCY, UnDefType.UNDEF);
147 Instant lastSeen = presenceDetection.getLastSeen();
148 if (value.isReachable() && lastSeen != null) {
149 updateState(CHANNEL_LASTSEEN, new DateTimeType(
150 ZonedDateTime.ofInstant(lastSeen, TimeZone.getDefault().toZoneId()).withFixedOffsetZone()));
151 } else if (!value.isReachable() && lastSeen == null) {
152 updateState(CHANNEL_LASTSEEN, UnDefType.UNDEF);
155 updateNetworkProperties();
159 public void dispose() {
160 PresenceDetection detection = presenceDetection;
161 if (detection != null) {
162 detection.stopAutomaticRefresh();
164 presenceDetection = null;
168 * Initialize with a presenceDetection object.
169 * Used by testing for injecting.
171 void initialize(PresenceDetection presenceDetection) {
172 handlerConfiguration = getConfigAs(NetworkHandlerConfiguration.class);
174 this.presenceDetection = presenceDetection;
175 presenceDetection.setHostname(handlerConfiguration.hostname);
176 presenceDetection.setNetworkInterfaceNames(handlerConfiguration.networkInterfaceNames);
177 presenceDetection.setPreferResponseTimeAsLatency(configuration.preferResponseTimeAsLatency);
179 if (isTCPServiceDevice) {
180 Integer port = handlerConfiguration.port;
182 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No port configured!");
185 presenceDetection.setServicePorts(Set.of(port));
187 // It does not harm to send an additional UDP packet to a device,
188 // therefore we assume all ping devices are iOS devices. If this
189 // does not work for all users for some obscure reason, we can make
190 // this a thing configuration variable.
191 presenceDetection.setIOSDevice(true);
192 // Hand over binding configurations to the network service
193 presenceDetection.setUseDhcpSniffing(configuration.allowDHCPlisten);
194 presenceDetection.setUseIcmpPing(configuration.allowSystemPings);
195 presenceDetection.setUseArpPing(true, configuration.arpPingToolPath, configuration.arpPingUtilMethod);
198 this.retries = handlerConfiguration.retry.intValue();
199 presenceDetection.setRefreshInterval(Duration.ofMillis(handlerConfiguration.refreshInterval));
200 presenceDetection.setTimeout(Duration.ofMillis(handlerConfiguration.timeout));
202 wakeOnLanPacketSender = new WakeOnLanPacketSender(handlerConfiguration.macAddress,
203 handlerConfiguration.hostname, handlerConfiguration.port, handlerConfiguration.networkInterfaceNames);
205 updateStatus(ThingStatus.ONLINE);
206 presenceDetection.startAutomaticRefresh();
208 updateNetworkProperties();
211 private void updateNetworkProperties() {
212 // Update properties (after startAutomaticRefresh, to get the correct dhcp state)
213 Map<String, String> properties = editProperties();
214 properties.put(NetworkBindingConstants.PROPERTY_ARP_STATE, presenceDetection.getArpPingState());
215 properties.put(NetworkBindingConstants.PROPERTY_ICMP_STATE, presenceDetection.getIPPingState());
216 properties.put(NetworkBindingConstants.PROPERTY_PRESENCE_DETECTION_TYPE, "");
217 properties.put(NetworkBindingConstants.PROPERTY_IOS_WAKEUP, presenceDetection.isIOSdevice() ? "Yes" : "No");
218 properties.put(NetworkBindingConstants.PROPERTY_DHCP_STATE, presenceDetection.getDhcpState());
219 updateProperties(properties);
222 // Create a new network service and apply all configurations.
224 public void initialize() {
225 initialize(new PresenceDetection(this, scheduler,
226 Duration.ofMillis(configuration.cacheDeviceStateTimeInMS.intValue())));
230 * Returns true if this handler is for a TCP service device.
232 public boolean isTCPServiceDevice() {
233 return isTCPServiceDevice;
237 public void bindingConfigurationChanged() {
238 // Make sure that changed binding configuration is reflected
239 presenceDetection.setPreferResponseTimeAsLatency(configuration.preferResponseTimeAsLatency);
243 public Collection<Class<? extends ThingHandlerService>> getServices() {
244 return List.of(NetworkActions.class);
247 public void sendWakeOnLanPacketViaIp() {
248 // Hostname can't be null
249 wakeOnLanPacketSender.sendWakeOnLanPacketViaIp();
252 public void sendWakeOnLanPacketViaMac() {
253 if (handlerConfiguration.macAddress.isEmpty()) {
254 throw new IllegalStateException(
255 "Cannot send WoL packet because the 'macAddress' is not configured for " + thing.getUID());
257 wakeOnLanPacketSender.sendWakeOnLanPacketViaMac();