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.nobohub.internal;
15 import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.CHANNEL_HUB_ACTIVE_OVERRIDE_NAME;
16 import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_HOSTNAME;
17 import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_PRODUCTION_DATE;
18 import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.PROPERTY_SOFTWARE_VERSION;
19 import static org.openhab.binding.nobohub.internal.NoboHubBindingConstants.RECOMMENDED_KEEPALIVE_INTERVAL;
21 import java.time.Duration;
22 import java.util.Collection;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.nobohub.internal.connection.HubCommunicationThread;
28 import org.openhab.binding.nobohub.internal.connection.HubConnection;
29 import org.openhab.binding.nobohub.internal.discovery.NoboThingDiscoveryService;
30 import org.openhab.binding.nobohub.internal.model.Component;
31 import org.openhab.binding.nobohub.internal.model.ComponentRegister;
32 import org.openhab.binding.nobohub.internal.model.Hub;
33 import org.openhab.binding.nobohub.internal.model.NoboCommunicationException;
34 import org.openhab.binding.nobohub.internal.model.NoboDataException;
35 import org.openhab.binding.nobohub.internal.model.OverrideMode;
36 import org.openhab.binding.nobohub.internal.model.OverridePlan;
37 import org.openhab.binding.nobohub.internal.model.OverrideRegister;
38 import org.openhab.binding.nobohub.internal.model.SerialNumber;
39 import org.openhab.binding.nobohub.internal.model.Temperature;
40 import org.openhab.binding.nobohub.internal.model.WeekProfile;
41 import org.openhab.binding.nobohub.internal.model.WeekProfileRegister;
42 import org.openhab.binding.nobohub.internal.model.Zone;
43 import org.openhab.binding.nobohub.internal.model.ZoneRegister;
44 import org.openhab.core.library.types.StringType;
45 import org.openhab.core.thing.Bridge;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.binding.BaseBridgeHandler;
51 import org.openhab.core.thing.binding.ThingHandler;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.RefreshType;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
58 * The {@link NoboHubBridgeHandler} is responsible for handling commands, which are
59 * sent to one of the channels.
61 * @author Jørgen Austvik - Initial contribution
62 * @author Espen Fossen - Initial contribution
65 public class NoboHubBridgeHandler extends BaseBridgeHandler {
67 private final Logger logger = LoggerFactory.getLogger(NoboHubBridgeHandler.class);
68 private @Nullable HubCommunicationThread hubThread;
69 private @Nullable NoboThingDiscoveryService discoveryService;
70 private @Nullable Hub hub;
72 private final OverrideRegister overrideRegister = new OverrideRegister();
73 private final WeekProfileRegister weekProfileRegister = new WeekProfileRegister();
74 private final ZoneRegister zoneRegister = new ZoneRegister();
75 private final ComponentRegister componentRegister = new ComponentRegister();
77 public NoboHubBridgeHandler(Bridge bridge) {
82 public void handleCommand(ChannelUID channelUID, Command command) {
83 logger.info("Handle command {} for channel {}!", command.toFullString(), channelUID);
85 HubCommunicationThread ht = this.hubThread;
87 if (command instanceof RefreshType) {
90 ht.getConnection().refreshAll();
92 } catch (NoboCommunicationException noboEx) {
93 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
94 "@text/message.bridge.status.failed [\"" + noboEx.getMessage() + "\"]");
100 if (CHANNEL_HUB_ACTIVE_OVERRIDE_NAME.equals(channelUID.getId())) {
101 if (ht != null && h != null) {
102 if (command instanceof StringType stringCommand) {
103 logger.debug("Changing override for hub {} to {}", channelUID, stringCommand);
105 OverrideMode mode = OverrideMode.getByName(stringCommand.toFullString());
106 ht.getConnection().setOverride(h, mode);
107 } catch (NoboCommunicationException nce) {
108 logger.debug("Failed setting override mode", nce);
109 } catch (NoboDataException nde) {
110 logger.debug("Date format error setting override mode", nde);
113 logger.debug("Command of wrong type: {} ({})", command, command.getClass().getName());
117 logger.debug("Could not set override, hub not detected yet");
121 logger.debug("Could not set override, hub connection thread not set up yet");
128 public void initialize() {
129 NoboHubBridgeConfiguration config = getConfigAs(NoboHubBridgeConfiguration.class);
131 String serialNumber = config.serialNumber;
132 if (null == serialNumber) {
133 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/message.missing.serial");
137 String hostName = config.hostName;
138 if (null == hostName || hostName.isEmpty()) {
139 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
140 "@text/message.bridge.missing.hostname");
144 logger.debug("Looking for Hub {} at {}", config.serialNumber, config.hostName);
146 // Set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
147 updateStatus(ThingStatus.UNKNOWN);
149 // Background handshake:
150 scheduler.execute(() -> {
152 HubConnection conn = new HubConnection(hostName, serialNumber, this);
155 logger.debug("Done connecting to {} ({})", hostName, serialNumber);
157 Duration timeout = RECOMMENDED_KEEPALIVE_INTERVAL;
158 if (config.pollingInterval > 0) {
159 timeout = Duration.ofSeconds(config.pollingInterval);
162 logger.debug("Starting communication thread to {}", hostName);
164 HubCommunicationThread ht = new HubCommunicationThread(conn, this, timeout);
168 if (ht.getConnection().isConnected()) {
169 logger.debug("Communication thread to {} is up and running, we are online", hostName);
170 updateProperty(Thing.PROPERTY_SERIAL_NUMBER, serialNumber);
171 updateStatus(ThingStatus.ONLINE);
173 logger.debug("HubCommunicationThread is not connected anymore, setting to OFFLINE");
174 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
175 "@text/message.bridge.connection.failed");
177 } catch (NoboCommunicationException commEx) {
178 logger.debug("HubCommunicationThread failed, exiting thread");
179 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, commEx.getMessage());
185 public void dispose() {
186 logger.debug("Disposing NoboHub '{}'", getThing().getUID().getId());
188 final NoboThingDiscoveryService discoveryService = this.discoveryService;
189 if (discoveryService != null) {
190 discoveryService.stopScan();
193 HubCommunicationThread ht = this.hubThread;
195 logger.debug("Stopping communication thread");
201 public void childHandlerInitialized(ThingHandler handler, Thing thing) {
202 logger.info("Adding thing: {}", thing.getLabel());
206 public void childHandlerDisposed(ThingHandler handler, Thing thing) {
207 logger.info("Disposing thing: {}", thing.getLabel());
210 private void onUpdate(Hub hub) {
211 logger.debug("Updating Hub: {}", hub.getName());
213 OverridePlan activeOverridePlan = getOverride(hub.getActiveOverrideId());
215 if (null != activeOverridePlan) {
216 logger.debug("Updating Hub with ActiveOverrideId {} with Name {}", activeOverridePlan.getId(),
217 activeOverridePlan.getMode().name());
219 updateState(NoboHubBindingConstants.CHANNEL_HUB_ACTIVE_OVERRIDE_NAME,
220 StringType.valueOf(activeOverridePlan.getMode().name()));
223 // Update all zones to set online status and update profile name from weekProfileRegister
224 for (Zone zone : zoneRegister.values()) {
228 Map<String, String> properties = editProperties();
229 properties.put(PROPERTY_HOSTNAME, hub.getName());
230 properties.put(Thing.PROPERTY_SERIAL_NUMBER, hub.getSerialNumber().toString());
231 properties.put(PROPERTY_SOFTWARE_VERSION, hub.getSoftwareVersion());
232 properties.put(Thing.PROPERTY_HARDWARE_VERSION, hub.getHardwareVersion());
233 properties.put(PROPERTY_PRODUCTION_DATE, hub.getProductionDate());
234 updateProperties(properties);
237 public void receivedData(@Nullable String line) {
240 } catch (NoboDataException nde) {
241 logger.debug("Failed parsing line '{}': {}", line, nde.getMessage());
245 private void parseLine(@Nullable String line) throws NoboDataException {
250 NoboThingDiscoveryService ds = this.discoveryService;
251 if (line.startsWith("H01")) {
252 Zone zone = Zone.fromH01(line);
253 zoneRegister.put(zone);
255 ds.detectZones(zoneRegister.values());
257 } else if (line.startsWith("H02")) {
258 Component component = Component.fromH02(line);
259 componentRegister.put(component);
261 ds.detectComponents(componentRegister.values());
263 } else if (line.startsWith("H03")) {
264 WeekProfile weekProfile = WeekProfile.fromH03(line);
265 weekProfileRegister.put(weekProfile);
266 } else if (line.startsWith("H04")) {
267 OverridePlan overridePlan = OverridePlan.fromH04(line);
268 overrideRegister.put(overridePlan);
269 } else if (line.startsWith("H05")) {
270 Hub hub = Hub.fromH05(line);
272 } else if (line.startsWith("S00")) {
273 Zone zone = Zone.fromH01(line);
274 zoneRegister.remove(zone.getId());
275 } else if (line.startsWith("S01")) {
276 Component component = Component.fromH02(line);
277 componentRegister.remove(component.getSerialNumber());
278 } else if (line.startsWith("S02")) {
279 WeekProfile weekProfile = WeekProfile.fromH03(line);
280 weekProfileRegister.remove(weekProfile.getId());
281 } else if (line.startsWith("S03")) {
282 OverridePlan overridePlan = OverridePlan.fromH04(line);
283 overrideRegister.remove(overridePlan.getId());
284 } else if (line.startsWith("B00")) {
285 Zone zone = Zone.fromH01(line);
286 zoneRegister.put(zone);
288 ds.detectZones(zoneRegister.values());
290 } else if (line.startsWith("B01")) {
291 Component component = Component.fromH02(line);
292 componentRegister.put(component);
294 ds.detectComponents(componentRegister.values());
296 } else if (line.startsWith("B02")) {
297 WeekProfile weekProfile = WeekProfile.fromH03(line);
298 weekProfileRegister.put(weekProfile);
299 } else if (line.startsWith("B03")) {
300 OverridePlan overridePlan = OverridePlan.fromH04(line);
301 overrideRegister.put(overridePlan);
302 } else if (line.startsWith("V00")) {
303 Zone zone = Zone.fromH01(line);
304 zoneRegister.put(zone);
306 } else if (line.startsWith("V01")) {
307 Component component = Component.fromH02(line);
308 componentRegister.put(component);
309 refreshComponent(component);
310 } else if (line.startsWith("V02")) {
311 WeekProfile weekProfile = WeekProfile.fromH03(line);
312 weekProfileRegister.put(weekProfile);
313 } else if (line.startsWith("V03")) {
314 Hub hub = Hub.fromH05(line);
316 } else if (line.startsWith("Y02")) {
317 Temperature temp = Temperature.fromY02(line);
318 Component component = getComponent(temp.getSerialNumber());
319 if (null != component) {
320 component.setTemperature(temp.getTemperature());
321 refreshComponent(component);
322 int zoneId = component.getTemperatureSensorForZoneId();
324 Zone zone = getZone(zoneId);
326 zone.setTemperature(temp.getTemperature());
331 } else if (line.startsWith("E00")) {
332 logger.debug("Error from Hub: {}", line);
334 // HANDSHAKE: Basic part of keepalive
335 // V06: Encryption key
336 // H00: contains no information
337 if (!line.startsWith("HANDSHAKE") && !line.startsWith("V06") && !line.startsWith("H00")) {
338 logger.info("Unknown information from Hub: '{}}'", line);
343 public @Nullable Zone getZone(Integer id) {
344 return zoneRegister.get(id);
347 public @Nullable WeekProfile getWeekProfile(Integer id) {
348 return weekProfileRegister.get(id);
351 public @Nullable Component getComponent(SerialNumber serialNumber) {
352 return componentRegister.get(serialNumber);
355 public @Nullable OverridePlan getOverride(Integer id) {
356 return overrideRegister.get(id);
359 public void sendCommand(String command) {
361 HubCommunicationThread ht = this.hubThread;
363 HubConnection conn = ht.getConnection();
364 conn.sendCommand(command);
368 private void refreshZone(Zone zone) {
369 this.getThing().getThings().forEach(thing -> {
370 if (thing.getHandler() instanceof ZoneHandler) {
371 ZoneHandler handler = (ZoneHandler) thing.getHandler();
372 if (handler != null && handler.getZoneId() == zone.getId()) {
373 handler.onUpdate(zone);
379 private void refreshComponent(Component component) {
380 this.getThing().getThings().forEach(thing -> {
381 if (thing.getHandler() instanceof ComponentHandler) {
382 ComponentHandler handler = (ComponentHandler) thing.getHandler();
383 if (handler != null) {
384 SerialNumber handlerSerial = handler.getSerialNumber();
385 if (handlerSerial != null && component.getSerialNumber().equals(handlerSerial)) {
386 handler.onUpdate(component);
393 public void startScan() {
396 HubCommunicationThread ht = this.hubThread;
398 ht.getConnection().refreshAll();
400 } catch (NoboCommunicationException noboEx) {
401 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
402 "@text/message.bridge.status.failed [\"" + noboEx.getMessage() + "\"]");
406 public void setDicsoveryService(NoboThingDiscoveryService discoveryService) {
407 this.discoveryService = discoveryService;
410 public Collection<WeekProfile> getWeekProfiles() {
411 return weekProfileRegister.values();
414 public void setStatusInfo(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
415 updateStatus(status, statusDetail, description);