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.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, configurationPid = "discovery.daikin")
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 @Nullable ScheduledFuture<?> backgroundFuture;
67 public DaikinACUnitDiscoveryService() {
68 super(Collections.singleton(DaikinBindingConstants.THING_TYPE_AC_UNIT), 600, true);
72 protected void startScan() {
73 scheduler.execute(this::scanner);
77 protected void startBackgroundDiscovery() {
78 logger.trace("Starting background discovery");
80 if (backgroundFuture != null && !backgroundFuture.isDone()) {
81 backgroundFuture.cancel(true);
82 backgroundFuture = null;
84 backgroundFuture = scheduler.scheduleWithFixedDelay(this::scanner, 0, 60, TimeUnit.SECONDS);
88 protected void stopBackgroundDiscovery() {
89 if (backgroundFuture != null && !backgroundFuture.isDone()) {
90 backgroundFuture.cancel(true);
91 backgroundFuture = null;
94 super.stopBackgroundDiscovery();
97 private void scanner() {
98 long timestampOfLastScan = getTimestampOfLastScan();
99 for (InetAddress broadcastAddress : getBroadcastAddresses()) {
100 logger.trace("Starting broadcast for {}", broadcastAddress.toString());
102 try (DatagramSocket socket = new DatagramSocket()) {
103 socket.setBroadcast(true);
104 socket.setReuseAddress(true);
105 byte[] packetContents = UDP_PACKET_CONTENTS.getBytes(StandardCharsets.UTF_8);
106 DatagramPacket packet = new DatagramPacket(packetContents, packetContents.length, broadcastAddress,
109 // Send before listening in case the port isn't bound until here.
112 // receivePacketAndDiscover will return false if no packet is received after 1 second
113 while (receivePacketAndDiscover(socket)) {
115 } catch (Exception e) {
116 // Nothing to do here - the host couldn't be found, likely because it doesn't exist
120 removeOlderResults(timestampOfLastScan);
123 private boolean receivePacketAndDiscover(DatagramSocket socket) {
125 byte[] buffer = new byte[512];
126 DatagramPacket incomingPacket = new DatagramPacket(buffer, buffer.length);
127 socket.setSoTimeout(1000 /* one second */);
128 socket.receive(incomingPacket);
130 String host = incomingPacket.getAddress().toString().substring(1);
131 String data = new String(incomingPacket.getData(), 0, incomingPacket.getLength(), "US-ASCII");
132 logger.trace("Received packet from {}: {}", host, data);
134 Map<String, String> parsedData = InfoParser.parse(data);
135 Boolean secure = "1".equals(parsedData.get("en_secure"));
136 String thingId = Optional.ofNullable(parsedData.get("ssid")).orElse(host.replace(".", "_"));
137 String mac = Optional.ofNullable(parsedData.get("mac")).orElse("");
138 String uuid = mac.isEmpty() ? UUID.randomUUID().toString()
139 : UUID.nameUUIDFromBytes(mac.getBytes()).toString();
141 DaikinWebTargets webTargets = new DaikinWebTargets(httpClient, host, secure, null);
142 boolean found = false;
144 // look for Daikin controller
146 found = "OK".equals(webTargets.getBasicInfo().ret);
147 } catch (DaikinCommunicationForbiddenException e) {
148 // At this point, we don't have the adapter's key nor a uuid
149 // so we're getting a Forbidden error
150 // let's discover it and let the user configure the Key
154 ThingUID thingUID = new ThingUID(DaikinBindingConstants.THING_TYPE_AC_UNIT, thingId);
155 DiscoveryResultBuilder resultBuilder = DiscoveryResultBuilder.create(thingUID)
156 .withProperty(DaikinConfiguration.HOST, host).withLabel("Daikin AC Unit (" + host + ")")
157 .withProperty(DaikinConfiguration.SECURE, secure)
158 .withRepresentationProperty(DaikinConfiguration.HOST);
160 resultBuilder = resultBuilder.withProperty(DaikinConfiguration.UUID, uuid);
162 DiscoveryResult result = resultBuilder.build();
164 logger.trace("Successfully discovered host {}", host);
165 thingDiscovered(result);
168 // look for Daikin Airbase controller
169 if ("OK".equals(webTargets.getAirbaseBasicInfo().ret)) {
170 ThingUID thingUID = new ThingUID(DaikinBindingConstants.THING_TYPE_AIRBASE_AC_UNIT, thingId);
171 DiscoveryResult result = DiscoveryResultBuilder.create(thingUID)
172 .withProperty(DaikinConfiguration.HOST, host).withLabel("Daikin Airbase AC Unit (" + host + ")")
173 .withRepresentationProperty(DaikinConfiguration.HOST).build();
175 logger.trace("Successfully discovered host {}", host);
176 thingDiscovered(result);
179 } catch (Exception e) {
182 // Shouldn't get here unless we don't detect a controller.
183 // Return true to continue with the next packet, which comes from another adapter
187 private List<InetAddress> getBroadcastAddresses() {
188 ArrayList<InetAddress> addresses = new ArrayList<>();
190 for (String broadcastAddress : NetUtil.getAllBroadcastAddresses()) {
192 addresses.add(InetAddress.getByName(broadcastAddress));
193 } catch (UnknownHostException e) {
194 logger.debug("Error broadcasting to {}", broadcastAddress, e);
202 protected void setDaikinHttpClientFactory(final DaikinHttpClientFactory httpClientFactory) {
203 this.httpClient = httpClientFactory.getHttpClient();