2 * Copyright (c) 2010-2022 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.daikin.internal.discovery;
15 import java.net.DatagramPacket;
16 import java.net.DatagramSocket;
17 import java.net.InetAddress;
18 import java.net.UnknownHostException;
19 import java.nio.charset.StandardCharsets;
20 import java.util.ArrayList;
21 import java.util.Collections;
22 import java.util.List;
24 import java.util.Optional;
25 import java.util.UUID;
26 import java.util.concurrent.ScheduledFuture;
27 import java.util.concurrent.TimeUnit;
29 import org.eclipse.jdt.annotation.NonNullByDefault;
30 import org.eclipse.jdt.annotation.Nullable;
31 import org.eclipse.jetty.client.HttpClient;
32 import org.openhab.binding.daikin.internal.DaikinBindingConstants;
33 import org.openhab.binding.daikin.internal.DaikinCommunicationForbiddenException;
34 import org.openhab.binding.daikin.internal.DaikinHttpClientFactory;
35 import org.openhab.binding.daikin.internal.DaikinWebTargets;
36 import org.openhab.binding.daikin.internal.api.InfoParser;
37 import org.openhab.binding.daikin.internal.config.DaikinConfiguration;
38 import org.openhab.core.config.discovery.AbstractDiscoveryService;
39 import org.openhab.core.config.discovery.DiscoveryResult;
40 import org.openhab.core.config.discovery.DiscoveryResultBuilder;
41 import org.openhab.core.config.discovery.DiscoveryService;
42 import org.openhab.core.net.NetUtil;
43 import org.openhab.core.thing.ThingUID;
44 import org.osgi.service.component.annotations.Component;
45 import org.osgi.service.component.annotations.Reference;
46 import org.slf4j.Logger;
47 import org.slf4j.LoggerFactory;
50 * Discovery service for Daikin AC units.
52 * @author Tim Waterhouse <tim@timwaterhouse.com> - Initial contribution
53 * @author Paul Smedley <paul@smedley.id.au> - Modifications to support Airbase Controllers
56 @Component(service = DiscoveryService.class)
58 public class DaikinACUnitDiscoveryService extends AbstractDiscoveryService {
59 private static final String UDP_PACKET_CONTENTS = "DAIKIN_UDP/common/basic_info";
60 private static final int REMOTE_UDP_PORT = 30050;
62 private Logger logger = LoggerFactory.getLogger(DaikinACUnitDiscoveryService.class);
64 private @Nullable HttpClient httpClient;
65 private final Runnable scanner;
66 private @Nullable ScheduledFuture<?> backgroundFuture;
68 public DaikinACUnitDiscoveryService() {
69 super(Collections.singleton(DaikinBindingConstants.THING_TYPE_AC_UNIT), 600, true);
70 scanner = createScanner();
74 protected void startScan() {
75 scheduler.execute(scanner);
79 protected void startBackgroundDiscovery() {
80 logger.debug("Starting background discovery");
82 if (backgroundFuture != null && !backgroundFuture.isDone()) {
83 backgroundFuture.cancel(true);
84 backgroundFuture = null;
86 backgroundFuture = scheduler.scheduleWithFixedDelay(scanner, 0, 60, TimeUnit.SECONDS);
90 protected void stopBackgroundDiscovery() {
91 if (backgroundFuture != null && !backgroundFuture.isDone()) {
92 backgroundFuture.cancel(true);
93 backgroundFuture = null;
96 super.stopBackgroundDiscovery();
99 private Runnable createScanner() {
101 long timestampOfLastScan = getTimestampOfLastScan();
102 for (InetAddress broadcastAddress : getBroadcastAddresses()) {
103 logger.debug("Starting broadcast for {}", broadcastAddress.toString());
105 try (DatagramSocket socket = new DatagramSocket()) {
106 socket.setBroadcast(true);
107 socket.setReuseAddress(true);
108 byte[] packetContents = UDP_PACKET_CONTENTS.getBytes(StandardCharsets.UTF_8);
109 DatagramPacket packet = new DatagramPacket(packetContents, packetContents.length, broadcastAddress,
112 // Send before listening in case the port isn't bound until here.
115 // receivePacketAndDiscover will return false if no packet is received after 1 second
116 while (receivePacketAndDiscover(socket)) {
118 } catch (Exception e) {
119 // Nothing to do here - the host couldn't be found, likely because it doesn't exist
123 removeOlderResults(timestampOfLastScan);
127 private boolean receivePacketAndDiscover(DatagramSocket socket) {
129 byte[] buffer = new byte[512];
130 DatagramPacket incomingPacket = new DatagramPacket(buffer, buffer.length);
131 socket.setSoTimeout(1000 /* one second */);
132 socket.receive(incomingPacket);
134 String host = incomingPacket.getAddress().toString().substring(1);
135 String data = new String(incomingPacket.getData(), 0, incomingPacket.getLength(), "US-ASCII");
136 logger.debug("Received packet from {}: {}", host, data);
138 Map<String, String> parsedData = InfoParser.parse(data);
139 Boolean secure = "1".equals(parsedData.get("en_secure"));
140 String thingId = Optional.ofNullable(parsedData.get("ssid")).orElse(host.replace(".", "_"));
141 String mac = Optional.ofNullable(parsedData.get("mac")).orElse("");
142 String uuid = mac.isEmpty() ? UUID.randomUUID().toString()
143 : UUID.nameUUIDFromBytes(mac.getBytes()).toString();
145 DaikinWebTargets webTargets = new DaikinWebTargets(httpClient, host, secure, null);
146 boolean found = false;
148 // look for Daikin controller
150 found = "OK".equals(webTargets.getBasicInfo().ret);
151 } catch (DaikinCommunicationForbiddenException e) {
152 // At this point, we don't have the adapter's key nor a uuid
153 // so we're getting a Forbidden error
154 // let's discover it and let the user configure the Key
158 ThingUID thingUID = new ThingUID(DaikinBindingConstants.THING_TYPE_AC_UNIT, thingId);
159 DiscoveryResultBuilder resultBuilder = DiscoveryResultBuilder.create(thingUID)
160 .withProperty(DaikinConfiguration.HOST, host).withLabel("Daikin AC Unit (" + host + ")")
161 .withProperty(DaikinConfiguration.SECURE, secure)
162 .withRepresentationProperty(DaikinConfiguration.HOST);
164 resultBuilder = resultBuilder.withProperty(DaikinConfiguration.UUID, uuid);
166 DiscoveryResult result = resultBuilder.build();
168 logger.debug("Successfully discovered host {}", host);
169 thingDiscovered(result);
172 // look for Daikin Airbase controller
173 if ("OK".equals(webTargets.getAirbaseBasicInfo().ret)) {
174 ThingUID thingUID = new ThingUID(DaikinBindingConstants.THING_TYPE_AIRBASE_AC_UNIT, thingId);
175 DiscoveryResult result = DiscoveryResultBuilder.create(thingUID)
176 .withProperty(DaikinConfiguration.HOST, host).withLabel("Daikin Airbase AC Unit (" + host + ")")
177 .withRepresentationProperty(DaikinConfiguration.HOST).build();
179 logger.debug("Successfully discovered host {}", host);
180 thingDiscovered(result);
183 } catch (Exception e) {
186 // Shouldn't get here unless we don't detect a controller.
187 // Return true to continue with the next packet, which comes from another adapter
191 private List<InetAddress> getBroadcastAddresses() {
192 ArrayList<InetAddress> addresses = new ArrayList<>();
194 for (String broadcastAddress : NetUtil.getAllBroadcastAddresses()) {
196 addresses.add(InetAddress.getByName(broadcastAddress));
197 } catch (UnknownHostException e) {
198 logger.debug("Error broadcasting to {}", broadcastAddress, e);
206 protected void setDaikinHttpClientFactory(final DaikinHttpClientFactory httpClientFactory) {
207 this.httpClient = httpClientFactory.getHttpClient();