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.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) {
103 StringType strCommand = (StringType) command;
104 logger.debug("Changing override for hub {} to {}", channelUID, strCommand);
106 OverrideMode mode = OverrideMode.getByName(strCommand.toFullString());
107 ht.getConnection().setOverride(h, mode);
108 } catch (NoboCommunicationException nce) {
109 logger.debug("Failed setting override mode", nce);
110 } catch (NoboDataException nde) {
111 logger.debug("Date format error setting override mode", nde);
114 logger.debug("Command of wrong type: {} ({})", command, command.getClass().getName());
118 logger.debug("Could not set override, hub not detected yet");
122 logger.debug("Could not set override, hub connection thread not set up yet");
129 public void initialize() {
130 NoboHubBridgeConfiguration config = getConfigAs(NoboHubBridgeConfiguration.class);
132 String serialNumber = config.serialNumber;
133 if (null == serialNumber) {
134 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "@text/message.missing.serial");
138 String hostName = config.hostName;
139 if (null == hostName || hostName.isEmpty()) {
140 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
141 "@text/message.bridge.missing.hostname");
145 logger.debug("Looking for Hub {} at {}", config.serialNumber, config.hostName);
147 // Set the thing status to UNKNOWN temporarily and let the background task decide for the real status.
148 updateStatus(ThingStatus.UNKNOWN);
150 // Background handshake:
151 scheduler.execute(() -> {
153 HubConnection conn = new HubConnection(hostName, serialNumber, this);
156 logger.debug("Done connecting to {} ({})", hostName, serialNumber);
158 Duration timeout = RECOMMENDED_KEEPALIVE_INTERVAL;
159 if (config.pollingInterval > 0) {
160 timeout = Duration.ofSeconds(config.pollingInterval);
163 logger.debug("Starting communication thread to {}", hostName);
165 HubCommunicationThread ht = new HubCommunicationThread(conn, this, timeout);
169 if (ht.getConnection().isConnected()) {
170 logger.debug("Communication thread to {} is up and running, we are online", hostName);
171 updateProperty(Thing.PROPERTY_SERIAL_NUMBER, serialNumber);
172 updateStatus(ThingStatus.ONLINE);
174 logger.debug("HubCommunicationThread is not connected anymore, setting to OFFLINE");
175 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
176 "@text/message.bridge.connection.failed");
178 } catch (NoboCommunicationException commEx) {
179 logger.debug("HubCommunicationThread failed, exiting thread");
180 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, commEx.getMessage());
186 public void dispose() {
187 logger.debug("Disposing NoboHub '{}'", getThing().getUID().getId());
189 final NoboThingDiscoveryService discoveryService = this.discoveryService;
190 if (discoveryService != null) {
191 discoveryService.stopScan();
194 HubCommunicationThread ht = this.hubThread;
196 logger.debug("Stopping communication thread");
202 public void childHandlerInitialized(ThingHandler handler, Thing thing) {
203 logger.info("Adding thing: {}", thing.getLabel());
207 public void childHandlerDisposed(ThingHandler handler, Thing thing) {
208 logger.info("Disposing thing: {}", thing.getLabel());
211 private void onUpdate(Hub hub) {
212 logger.debug("Updating Hub: {}", hub.getName());
214 OverridePlan activeOverridePlan = getOverride(hub.getActiveOverrideId());
216 if (null != activeOverridePlan) {
217 logger.debug("Updating Hub with ActiveOverrideId {} with Name {}", activeOverridePlan.getId(),
218 activeOverridePlan.getMode().name());
220 updateState(NoboHubBindingConstants.CHANNEL_HUB_ACTIVE_OVERRIDE_NAME,
221 StringType.valueOf(activeOverridePlan.getMode().name()));
224 // Update all zones to set online status and update profile name from weekProfileRegister
225 for (Zone zone : zoneRegister.values()) {
229 Map<String, String> properties = editProperties();
230 properties.put(PROPERTY_HOSTNAME, hub.getName());
231 properties.put(Thing.PROPERTY_SERIAL_NUMBER, hub.getSerialNumber().toString());
232 properties.put(PROPERTY_SOFTWARE_VERSION, hub.getSoftwareVersion());
233 properties.put(Thing.PROPERTY_HARDWARE_VERSION, hub.getHardwareVersion());
234 properties.put(PROPERTY_PRODUCTION_DATE, hub.getProductionDate());
235 updateProperties(properties);
238 public void receivedData(@Nullable String line) {
241 } catch (NoboDataException nde) {
242 logger.debug("Failed parsing line '{}': {}", line, nde.getMessage());
246 private void parseLine(@Nullable String line) throws NoboDataException {
251 NoboThingDiscoveryService ds = this.discoveryService;
252 if (line.startsWith("H01")) {
253 Zone zone = Zone.fromH01(line);
254 zoneRegister.put(zone);
256 ds.detectZones(zoneRegister.values());
258 } else if (line.startsWith("H02")) {
259 Component component = Component.fromH02(line);
260 componentRegister.put(component);
262 ds.detectComponents(componentRegister.values());
264 } else if (line.startsWith("H03")) {
265 WeekProfile weekProfile = WeekProfile.fromH03(line);
266 weekProfileRegister.put(weekProfile);
267 } else if (line.startsWith("H04")) {
268 OverridePlan overridePlan = OverridePlan.fromH04(line);
269 overrideRegister.put(overridePlan);
270 } else if (line.startsWith("H05")) {
271 Hub hub = Hub.fromH05(line);
273 } else if (line.startsWith("S00")) {
274 Zone zone = Zone.fromH01(line);
275 zoneRegister.remove(zone.getId());
276 } else if (line.startsWith("S01")) {
277 Component component = Component.fromH02(line);
278 componentRegister.remove(component.getSerialNumber());
279 } else if (line.startsWith("S02")) {
280 WeekProfile weekProfile = WeekProfile.fromH03(line);
281 weekProfileRegister.remove(weekProfile.getId());
282 } else if (line.startsWith("S03")) {
283 OverridePlan overridePlan = OverridePlan.fromH04(line);
284 overrideRegister.remove(overridePlan.getId());
285 } else if (line.startsWith("B00")) {
286 Zone zone = Zone.fromH01(line);
287 zoneRegister.put(zone);
289 ds.detectZones(zoneRegister.values());
291 } else if (line.startsWith("B01")) {
292 Component component = Component.fromH02(line);
293 componentRegister.put(component);
295 ds.detectComponents(componentRegister.values());
297 } else if (line.startsWith("B02")) {
298 WeekProfile weekProfile = WeekProfile.fromH03(line);
299 weekProfileRegister.put(weekProfile);
300 } else if (line.startsWith("B03")) {
301 OverridePlan overridePlan = OverridePlan.fromH04(line);
302 overrideRegister.put(overridePlan);
303 } else if (line.startsWith("V00")) {
304 Zone zone = Zone.fromH01(line);
305 zoneRegister.put(zone);
307 } else if (line.startsWith("V01")) {
308 Component component = Component.fromH02(line);
309 componentRegister.put(component);
310 refreshComponent(component);
311 } else if (line.startsWith("V02")) {
312 WeekProfile weekProfile = WeekProfile.fromH03(line);
313 weekProfileRegister.put(weekProfile);
314 } else if (line.startsWith("V03")) {
315 Hub hub = Hub.fromH05(line);
317 } else if (line.startsWith("Y02")) {
318 Temperature temp = Temperature.fromY02(line);
319 Component component = getComponent(temp.getSerialNumber());
320 if (null != component) {
321 component.setTemperature(temp.getTemperature());
322 refreshComponent(component);
323 int zoneId = component.getTemperatureSensorForZoneId();
325 Zone zone = getZone(zoneId);
327 zone.setTemperature(temp.getTemperature());
332 } else if (line.startsWith("E00")) {
333 logger.debug("Error from Hub: {}", line);
335 // HANDSHAKE: Basic part of keepalive
336 // V06: Encryption key
337 // H00: contains no information
338 if (!line.startsWith("HANDSHAKE") && !line.startsWith("V06") && !line.startsWith("H00")) {
339 logger.info("Unknown information from Hub: '{}}'", line);
344 public @Nullable Zone getZone(Integer id) {
345 return zoneRegister.get(id);
348 public @Nullable WeekProfile getWeekProfile(Integer id) {
349 return weekProfileRegister.get(id);
352 public @Nullable Component getComponent(SerialNumber serialNumber) {
353 return componentRegister.get(serialNumber);
356 public @Nullable OverridePlan getOverride(Integer id) {
357 return overrideRegister.get(id);
360 public void sendCommand(String command) {
362 HubCommunicationThread ht = this.hubThread;
364 HubConnection conn = ht.getConnection();
365 conn.sendCommand(command);
369 private void refreshZone(Zone zone) {
370 this.getThing().getThings().forEach(thing -> {
371 if (thing.getHandler() instanceof ZoneHandler) {
372 ZoneHandler handler = (ZoneHandler) thing.getHandler();
373 if (handler != null && handler.getZoneId() == zone.getId()) {
374 handler.onUpdate(zone);
380 private void refreshComponent(Component component) {
381 this.getThing().getThings().forEach(thing -> {
382 if (thing.getHandler() instanceof ComponentHandler) {
383 ComponentHandler handler = (ComponentHandler) thing.getHandler();
384 if (handler != null) {
385 SerialNumber handlerSerial = handler.getSerialNumber();
386 if (handlerSerial != null && component.getSerialNumber().equals(handlerSerial)) {
387 handler.onUpdate(component);
394 public void startScan() {
397 HubCommunicationThread ht = this.hubThread;
399 ht.getConnection().refreshAll();
401 } catch (NoboCommunicationException noboEx) {
402 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
403 "@text/message.bridge.status.failed [\"" + noboEx.getMessage() + "\"]");
407 public void setDicsoveryService(NoboThingDiscoveryService discoveryService) {
408 this.discoveryService = discoveryService;
411 public Collection<WeekProfile> getWeekProfiles() {
412 return weekProfileRegister.values();
415 public void setStatusInfo(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
416 updateStatus(status, statusDetail, description);