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.*;
17 import java.time.Instant;
18 import java.time.ZonedDateTime;
19 import java.util.Collection;
20 import java.util.List;
23 import java.util.TimeZone;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.openhab.binding.network.internal.NetworkBindingConfiguration;
27 import org.openhab.binding.network.internal.NetworkBindingConfigurationListener;
28 import org.openhab.binding.network.internal.NetworkBindingConstants;
29 import org.openhab.binding.network.internal.NetworkHandlerConfiguration;
30 import org.openhab.binding.network.internal.PresenceDetection;
31 import org.openhab.binding.network.internal.PresenceDetectionListener;
32 import org.openhab.binding.network.internal.PresenceDetectionValue;
33 import org.openhab.binding.network.internal.WakeOnLanPacketSender;
34 import org.openhab.binding.network.internal.action.NetworkActions;
35 import org.openhab.core.library.types.DateTimeType;
36 import org.openhab.core.library.types.DecimalType;
37 import org.openhab.core.library.types.OnOffType;
38 import org.openhab.core.library.types.QuantityType;
39 import org.openhab.core.library.unit.MetricPrefix;
40 import org.openhab.core.library.unit.Units;
41 import org.openhab.core.thing.ChannelUID;
42 import org.openhab.core.thing.Thing;
43 import org.openhab.core.thing.ThingStatus;
44 import org.openhab.core.thing.ThingStatusDetail;
45 import org.openhab.core.thing.binding.BaseThingHandler;
46 import org.openhab.core.thing.binding.ThingHandlerService;
47 import org.openhab.core.types.Command;
48 import org.openhab.core.types.RefreshType;
49 import org.openhab.core.types.UnDefType;
50 import org.slf4j.Logger;
51 import org.slf4j.LoggerFactory;
54 * This handler is handling the CHANNEL_ONLINE (boolean) and CHANNEL_TIME (time in ms)
55 * commands and is starting a {@see NetworkService} instance for the configured device.
57 * @author Marc Mettke - Initial contribution
58 * @author David Graeff - Rewritten
59 * @author Wouter Born - Add Wake-on-LAN thing action support
62 public class NetworkHandler extends BaseThingHandler
63 implements PresenceDetectionListener, NetworkBindingConfigurationListener {
64 private final Logger logger = LoggerFactory.getLogger(NetworkHandler.class);
65 private @NonNullByDefault({}) PresenceDetection presenceDetection;
66 private @NonNullByDefault({}) WakeOnLanPacketSender wakeOnLanPacketSender;
68 private boolean isTCPServiceDevice;
69 private NetworkBindingConfiguration configuration;
71 // How many retries before a device is deemed offline
73 // Retry counter. Will be reset as soon as a device presence detection succeed.
74 private int retryCounter = 0;
75 private NetworkHandlerConfiguration handlerConfiguration = new NetworkHandlerConfiguration();
78 * Do not call this directly, but use the {@see NetworkHandlerBuilder} instead.
80 public NetworkHandler(Thing thing, boolean isTCPServiceDevice, NetworkBindingConfiguration configuration) {
82 this.isTCPServiceDevice = isTCPServiceDevice;
83 this.configuration = configuration;
84 this.configuration.addNetworkBindingConfigurationListener(this);
87 private void refreshValue(ChannelUID channelUID) {
88 // We are not yet even initialized, don't do anything
89 if (presenceDetection == null || !presenceDetection.isAutomaticRefreshing()) {
93 switch (channelUID.getId()) {
95 presenceDetection.getValue(value -> updateState(CHANNEL_ONLINE, OnOffType.from(value.isReachable())));
98 case CHANNEL_DEPRECATED_TIME:
99 presenceDetection.getValue(value -> {
100 updateState(CHANNEL_LATENCY,
101 new QuantityType<>(value.getLowestLatency(), MetricPrefix.MILLI(Units.SECOND)));
102 updateState(CHANNEL_DEPRECATED_TIME, new DecimalType(value.getLowestLatency()));
105 case CHANNEL_LASTSEEN:
106 if (presenceDetection.getLastSeen() > 0) {
107 Instant instant = Instant.ofEpochMilli(presenceDetection.getLastSeen());
108 updateState(CHANNEL_LASTSEEN, new DateTimeType(
109 ZonedDateTime.ofInstant(instant, TimeZone.getDefault().toZoneId()).withFixedOffsetZone()));
111 updateState(CHANNEL_LASTSEEN, UnDefType.UNDEF);
115 logger.debug("Command received for an unknown channel: {}", channelUID.getId());
121 public void handleCommand(ChannelUID channelUID, Command command) {
122 if (command instanceof RefreshType) {
123 refreshValue(channelUID);
125 logger.debug("Command {} is not supported for channel: {}", command, channelUID.getId());
130 public void partialDetectionResult(PresenceDetectionValue value) {
131 updateState(CHANNEL_ONLINE, OnOffType.ON);
132 updateState(CHANNEL_LATENCY, new QuantityType<>(value.getLowestLatency(), MetricPrefix.MILLI(Units.SECOND)));
133 updateState(CHANNEL_DEPRECATED_TIME, new DecimalType(value.getLowestLatency()));
137 public void finalDetectionResult(PresenceDetectionValue value) {
138 // We do not notify the framework immediately if a device presence detection failed and
139 // the user configured retries to be > 1.
140 retryCounter = !value.isReachable() ? retryCounter + 1 : 0;
142 if (retryCounter >= this.retries) {
143 updateState(CHANNEL_ONLINE, OnOffType.OFF);
144 updateState(CHANNEL_LATENCY, UnDefType.UNDEF);
145 updateState(CHANNEL_DEPRECATED_TIME, UnDefType.UNDEF);
149 if (value.isReachable()) {
150 Instant instant = Instant.ofEpochMilli(presenceDetection.getLastSeen());
151 updateState(CHANNEL_LASTSEEN, new DateTimeType(
152 ZonedDateTime.ofInstant(instant, TimeZone.getDefault().toZoneId()).withFixedOffsetZone()));
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.setPreferResponseTimeAsLatency(configuration.preferResponseTimeAsLatency);
178 if (isTCPServiceDevice) {
179 Integer port = handlerConfiguration.port;
181 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No port configured!");
184 presenceDetection.setServicePorts(Set.of(port));
186 // It does not harm to send an additional UDP packet to a device,
187 // therefore we assume all ping devices are iOS devices. If this
188 // does not work for all users for some obscure reason, we can make
189 // this a thing configuration variable.
190 presenceDetection.setIOSDevice(true);
191 // Hand over binding configurations to the network service
192 presenceDetection.setUseDhcpSniffing(configuration.allowDHCPlisten);
193 presenceDetection.setUseIcmpPing(configuration.allowSystemPings);
194 presenceDetection.setUseArpPing(true, configuration.arpPingToolPath, configuration.arpPingUtilMethod);
197 this.retries = handlerConfiguration.retry.intValue();
198 presenceDetection.setRefreshInterval(handlerConfiguration.refreshInterval.longValue());
199 presenceDetection.setTimeout(handlerConfiguration.timeout.intValue());
201 wakeOnLanPacketSender = new WakeOnLanPacketSender(handlerConfiguration.macAddress,
202 handlerConfiguration.hostname, handlerConfiguration.port);
204 updateStatus(ThingStatus.ONLINE);
205 presenceDetection.startAutomaticRefresh(scheduler);
207 updateNetworkProperties();
210 private void updateNetworkProperties() {
211 // Update properties (after startAutomaticRefresh, to get the correct dhcp state)
212 Map<String, String> properties = editProperties();
213 properties.put(NetworkBindingConstants.PROPERTY_ARP_STATE, presenceDetection.getArpPingState());
214 properties.put(NetworkBindingConstants.PROPERTY_ICMP_STATE, presenceDetection.getIPPingState());
215 properties.put(NetworkBindingConstants.PROPERTY_PRESENCE_DETECTION_TYPE, "");
216 properties.put(NetworkBindingConstants.PROPERTY_IOS_WAKEUP, presenceDetection.isIOSdevice() ? "Yes" : "No");
217 properties.put(NetworkBindingConstants.PROPERTY_DHCP_STATE, presenceDetection.getDhcpState());
218 updateProperties(properties);
221 // Create a new network service and apply all configurations.
223 public void initialize() {
224 initialize(new PresenceDetection(this, configuration.cacheDeviceStateTimeInMS.intValue()));
228 * Returns true if this handler is for a TCP service device.
230 public boolean isTCPServiceDevice() {
231 return isTCPServiceDevice;
235 public void bindingConfigurationChanged() {
236 // Make sure that changed binding configuration is reflected
237 presenceDetection.setPreferResponseTimeAsLatency(configuration.preferResponseTimeAsLatency);
241 public Collection<Class<? extends ThingHandlerService>> getServices() {
242 return List.of(NetworkActions.class);
245 public void sendWakeOnLanPacketViaIp() {
246 // Hostname can't be null
247 wakeOnLanPacketSender.sendWakeOnLanPacketViaIp();
250 public void sendWakeOnLanPacketViaMac() {
251 if (handlerConfiguration.macAddress.isEmpty()) {
252 throw new IllegalStateException(
253 "Cannot send WoL packet because the 'macAddress' is not configured for " + thing.getUID());
255 wakeOnLanPacketSender.sendWakeOnLanPacketViaMac();