]> git.basschouten.com Git - openhab-addons.git/blob
469be159e0f79d0d3f24463bb3a8ffdf7aa85700
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.network.internal.handler;
14
15 import static org.openhab.binding.network.internal.NetworkBindingConstants.*;
16 import static org.openhab.binding.network.internal.utils.NetworkUtils.durationToMillis;
17
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;
23 import java.util.Map;
24 import java.util.Set;
25 import java.util.TimeZone;
26
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;
53
54 /**
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.
57  *
58  * @author Marc Mettke - Initial contribution
59  * @author David Graeff - Rewritten
60  * @author Wouter Born - Add Wake-on-LAN thing action support
61  */
62 @NonNullByDefault
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;
68
69     private boolean isTCPServiceDevice;
70     private NetworkBindingConfiguration configuration;
71
72     // How many retries before a device is deemed offline
73     int retries;
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();
77
78     /**
79      * Do not call this directly, but use the {@see NetworkHandlerBuilder} instead.
80      */
81     public NetworkHandler(Thing thing, boolean isTCPServiceDevice, NetworkBindingConfiguration configuration) {
82         super(thing);
83         this.isTCPServiceDevice = isTCPServiceDevice;
84         this.configuration = configuration;
85         this.configuration.addNetworkBindingConfigurationListener(this);
86     }
87
88     private void refreshValue(ChannelUID channelUID) {
89         // We are not yet even initialized, don't do anything
90         if (presenceDetection == null || !presenceDetection.isAutomaticRefreshing()) {
91             return;
92         }
93
94         switch (channelUID.getId()) {
95             case CHANNEL_ONLINE:
96                 presenceDetection.getValue(value -> updateState(CHANNEL_ONLINE, OnOffType.from(value.isReachable())));
97                 break;
98             case CHANNEL_LATENCY:
99                 presenceDetection.getValue(value -> {
100                     double latencyMs = durationToMillis(value.getLowestLatency());
101                     updateState(CHANNEL_LATENCY, new QuantityType<>(latencyMs, MetricPrefix.MILLI(Units.SECOND)));
102                 });
103                 break;
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()));
109                 } else {
110                     updateState(CHANNEL_LASTSEEN, UnDefType.UNDEF);
111                 }
112                 break;
113             default:
114                 logger.debug("Command received for an unknown channel: {}", channelUID.getId());
115                 break;
116         }
117     }
118
119     @Override
120     public void handleCommand(ChannelUID channelUID, Command command) {
121         if (command instanceof RefreshType) {
122             refreshValue(channelUID);
123         } else {
124             logger.debug("Command {} is not supported for channel: {}", command, channelUID.getId());
125         }
126     }
127
128     @Override
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)));
133     }
134
135     @Override
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;
140
141         if (retryCounter >= retries) {
142             updateState(CHANNEL_ONLINE, OnOffType.OFF);
143             updateState(CHANNEL_LATENCY, UnDefType.UNDEF);
144             retryCounter = 0;
145         }
146
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);
153         }
154
155         updateNetworkProperties();
156     }
157
158     @Override
159     public void dispose() {
160         PresenceDetection detection = presenceDetection;
161         if (detection != null) {
162             detection.stopAutomaticRefresh();
163         }
164         presenceDetection = null;
165     }
166
167     /**
168      * Initialize with a presenceDetection object.
169      * Used by testing for injecting.
170      */
171     void initialize(PresenceDetection presenceDetection) {
172         handlerConfiguration = getConfigAs(NetworkHandlerConfiguration.class);
173
174         this.presenceDetection = presenceDetection;
175         presenceDetection.setHostname(handlerConfiguration.hostname);
176         presenceDetection.setNetworkInterfaceNames(handlerConfiguration.networkInterfaceNames);
177         presenceDetection.setPreferResponseTimeAsLatency(configuration.preferResponseTimeAsLatency);
178
179         if (isTCPServiceDevice) {
180             Integer port = handlerConfiguration.port;
181             if (port == null) {
182                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "No port configured!");
183                 return;
184             }
185             presenceDetection.setServicePorts(Set.of(port));
186         } else {
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);
196         }
197
198         this.retries = handlerConfiguration.retry.intValue();
199         presenceDetection.setRefreshInterval(Duration.ofMillis(handlerConfiguration.refreshInterval));
200         presenceDetection.setTimeout(Duration.ofMillis(handlerConfiguration.timeout));
201
202         wakeOnLanPacketSender = new WakeOnLanPacketSender(handlerConfiguration.macAddress,
203                 handlerConfiguration.hostname, handlerConfiguration.port, handlerConfiguration.networkInterfaceNames);
204
205         updateStatus(ThingStatus.ONLINE);
206         presenceDetection.startAutomaticRefresh();
207
208         updateNetworkProperties();
209     }
210
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);
220     }
221
222     // Create a new network service and apply all configurations.
223     @Override
224     public void initialize() {
225         initialize(new PresenceDetection(this, scheduler,
226                 Duration.ofMillis(configuration.cacheDeviceStateTimeInMS.intValue())));
227     }
228
229     /**
230      * Returns true if this handler is for a TCP service device.
231      */
232     public boolean isTCPServiceDevice() {
233         return isTCPServiceDevice;
234     }
235
236     @Override
237     public void bindingConfigurationChanged() {
238         // Make sure that changed binding configuration is reflected
239         presenceDetection.setPreferResponseTimeAsLatency(configuration.preferResponseTimeAsLatency);
240     }
241
242     @Override
243     public Collection<Class<? extends ThingHandlerService>> getServices() {
244         return List.of(NetworkActions.class);
245     }
246
247     public void sendWakeOnLanPacketViaIp() {
248         // Hostname can't be null
249         wakeOnLanPacketSender.sendWakeOnLanPacketViaIp();
250     }
251
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());
256         }
257         wakeOnLanPacketSender.sendWakeOnLanPacketViaMac();
258     }
259 }