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.nikohomecontrol.internal.handler;
15 import static org.openhab.binding.nikohomecontrol.internal.NikoHomeControlBindingConstants.*;
16 import static org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlConstants.*;
17 import static org.openhab.core.library.unit.SIUnits.CELSIUS;
18 import static org.openhab.core.types.RefreshType.REFRESH;
20 import java.math.BigDecimal;
21 import java.util.HashMap;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import org.eclipse.jdt.annotation.NonNullByDefault;
27 import org.eclipse.jdt.annotation.Nullable;
28 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcThermostat;
29 import org.openhab.binding.nikohomecontrol.internal.protocol.NhcThermostatEvent;
30 import org.openhab.binding.nikohomecontrol.internal.protocol.NikoHomeControlCommunication;
31 import org.openhab.binding.nikohomecontrol.internal.protocol.nhc2.NhcThermostat2;
32 import org.openhab.core.library.types.DecimalType;
33 import org.openhab.core.library.types.QuantityType;
34 import org.openhab.core.library.types.StringType;
35 import org.openhab.core.thing.Bridge;
36 import org.openhab.core.thing.ChannelUID;
37 import org.openhab.core.thing.Thing;
38 import org.openhab.core.thing.ThingStatus;
39 import org.openhab.core.thing.ThingStatusDetail;
40 import org.openhab.core.thing.ThingStatusInfo;
41 import org.openhab.core.thing.binding.BaseThingHandler;
42 import org.openhab.core.types.Command;
43 import org.slf4j.Logger;
44 import org.slf4j.LoggerFactory;
47 * The {@link NikoHomeControlThermostatHandler} is responsible for handling commands, which are
48 * sent to one of the channels.
50 * @author Mark Herwege - Initial Contribution
53 public class NikoHomeControlThermostatHandler extends BaseThingHandler implements NhcThermostatEvent {
55 private final Logger logger = LoggerFactory.getLogger(NikoHomeControlThermostatHandler.class);
57 private volatile @Nullable NhcThermostat nhcThermostat;
59 private volatile boolean initialized = false;
61 private String thermostatId = "";
62 private int overruleTime;
64 private volatile @Nullable ScheduledFuture<?> refreshTimer; // used to refresh the remaining overrule time every
67 public NikoHomeControlThermostatHandler(Thing thing) {
72 public void handleCommand(ChannelUID channelUID, Command command) {
73 NikoHomeControlCommunication nhcComm = getCommunication(getBridgeHandler());
74 if (nhcComm == null) {
75 logger.debug("communication not up yet, cannot handle command {} for {}", command, channelUID);
79 // This can be expensive, therefore do it in a job.
80 scheduler.submit(() -> {
81 if (!nhcComm.communicationActive()) {
82 restartCommunication(nhcComm);
85 if (nhcComm.communicationActive()) {
86 handleCommandSelection(channelUID, command);
91 private void handleCommandSelection(ChannelUID channelUID, Command command) {
92 NhcThermostat nhcThermostat = this.nhcThermostat;
93 if (nhcThermostat == null) {
94 logger.debug("thermostat with ID {} not initialized", thermostatId);
98 logger.debug("handle command {} for {}", command, channelUID);
100 if (REFRESH.equals(command)) {
101 thermostatEvent(nhcThermostat.getMeasured(), nhcThermostat.getSetpoint(), nhcThermostat.getMode(),
102 nhcThermostat.getOverrule(), nhcThermostat.getDemand());
106 switch (channelUID.getId()) {
107 case CHANNEL_MEASURED:
109 case CHANNEL_HEATING_DEMAND:
110 updateStatus(ThingStatus.ONLINE);
113 if (command instanceof DecimalType decimalCommand) {
114 nhcThermostat.executeMode(decimalCommand.intValue());
116 updateStatus(ThingStatus.ONLINE);
118 case CHANNEL_HEATING_MODE:
119 if (command instanceof StringType) {
120 nhcThermostat.executeMode(command.toString());
122 updateStatus(ThingStatus.ONLINE);
124 case CHANNEL_SETPOINT:
125 // Always set the new setpoint temperature as an overrule
126 // If no overrule time is given yet, set the overrule time to the configuration parameter
127 int time = nhcThermostat.getOverruletime();
131 if (command instanceof QuantityType<?> quantityCommand) {
132 QuantityType<?> setpoint = quantityCommand.toUnit(CELSIUS);
133 if (setpoint != null) {
134 nhcThermostat.executeOverrule(Math.round(setpoint.floatValue() * 10), time);
136 } else if (command instanceof DecimalType decimalCommand) {
137 BigDecimal setpoint = decimalCommand.toBigDecimal();
138 nhcThermostat.executeOverrule(Math.round(setpoint.floatValue() * 10), time);
140 updateStatus(ThingStatus.ONLINE);
142 case CHANNEL_OVERRULETIME:
143 if (command instanceof DecimalType decimalCommand) {
144 int overruletime = decimalCommand.intValue();
145 int overrule = nhcThermostat.getOverrule();
146 if (overruletime <= 0) {
150 nhcThermostat.executeOverrule(overrule, overruletime);
152 updateStatus(ThingStatus.ONLINE);
155 logger.debug("unexpected command for channel {}", channelUID.getId());
160 public void initialize() {
163 NikoHomeControlThermostatConfig config = getConfig().as(NikoHomeControlThermostatConfig.class);
165 thermostatId = config.thermostatId;
166 overruleTime = config.overruleTime;
168 NikoHomeControlBridgeHandler bridgeHandler = getBridgeHandler();
169 if (bridgeHandler == null) {
170 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
171 "@text/offline.configuration-error.invalid-bridge-handler");
175 updateStatus(ThingStatus.UNKNOWN);
177 Bridge bridge = getBridge();
178 if ((bridge != null) && ThingStatus.ONLINE.equals(bridge.getStatus())) {
179 // We need to do this in a separate thread because we may have to wait for the
180 // communication to become active
181 scheduler.submit(this::startCommunication);
185 private synchronized void startCommunication() {
186 NikoHomeControlCommunication nhcComm = getCommunication(getBridgeHandler());
188 if (nhcComm == null) {
192 if (!nhcComm.communicationActive()) {
193 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
194 "@text/offline.communication-error");
198 NhcThermostat nhcThermostat = nhcComm.getThermostats().get(thermostatId);
199 if (nhcThermostat == null) {
200 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
201 "@text/offline.configuration-error.thermostatId");
205 nhcThermostat.setEventHandler(this);
207 updateProperties(nhcThermostat);
209 String thermostatLocation = nhcThermostat.getLocation();
210 if (thing.getLocation() == null) {
211 thing.setLocation(thermostatLocation);
214 this.nhcThermostat = nhcThermostat;
216 logger.debug("thermostat intialized {}", thermostatId);
218 Bridge bridge = getBridge();
219 if ((bridge != null) && (bridge.getStatus() == ThingStatus.ONLINE)) {
220 updateStatus(ThingStatus.ONLINE);
222 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
225 thermostatEvent(nhcThermostat.getMeasured(), nhcThermostat.getSetpoint(), nhcThermostat.getMode(),
226 nhcThermostat.getOverrule(), nhcThermostat.getDemand());
232 public void dispose() {
233 NikoHomeControlCommunication nhcComm = getCommunication(getBridgeHandler());
234 if (nhcComm != null) {
235 NhcThermostat thermostat = nhcComm.getThermostats().get(thermostatId);
236 if (thermostat != null) {
237 thermostat.unsetEventHandler();
240 nhcThermostat = null;
244 private void updateProperties(NhcThermostat nhcThermostat) {
245 Map<String, String> properties = new HashMap<>();
247 if (nhcThermostat instanceof NhcThermostat2 thermostat) {
248 properties.put(PROPERTY_DEVICE_TYPE, thermostat.getDeviceType());
249 properties.put(PROPERTY_DEVICE_TECHNOLOGY, thermostat.getDeviceTechnology());
250 properties.put(PROPERTY_DEVICE_MODEL, thermostat.getDeviceModel());
253 thing.setProperties(properties);
257 public void thermostatEvent(int measured, int setpoint, int mode, int overrule, int demand) {
258 NhcThermostat nhcThermostat = this.nhcThermostat;
259 if (nhcThermostat == null) {
260 logger.debug("thermostat with ID {} not initialized", thermostatId);
264 updateState(CHANNEL_MEASURED, new QuantityType<>(measured / 10.0, CELSIUS));
266 int overruletime = nhcThermostat.getRemainingOverruletime();
267 updateState(CHANNEL_OVERRULETIME, new DecimalType(overruletime));
268 // refresh the remaining time every minute
269 scheduleRefreshOverruletime(nhcThermostat);
271 // If there is an overrule temperature set, use this in the setpoint channel, otherwise use the original
272 // setpoint temperature
273 if (overruletime == 0) {
274 updateState(CHANNEL_SETPOINT, new QuantityType<>(setpoint / 10.0, CELSIUS));
276 updateState(CHANNEL_SETPOINT, new QuantityType<>(overrule / 10.0, CELSIUS));
279 updateState(CHANNEL_MODE, new DecimalType(mode));
280 updateState(CHANNEL_HEATING_MODE, new StringType(THERMOSTATMODES[mode]));
282 updateState(CHANNEL_DEMAND, new DecimalType(demand));
283 updateState(CHANNEL_HEATING_DEMAND, new StringType(THERMOSTATDEMAND[Math.abs(demand) <= 1 ? (demand + 1) : 0]));
285 updateStatus(ThingStatus.ONLINE);
289 * Method to update state of overruletime channel every minute with remaining time.
291 * @param NhcThermostat object
294 private void scheduleRefreshOverruletime(NhcThermostat nhcThermostat) {
295 cancelRefreshTimer();
297 if (nhcThermostat.getRemainingOverruletime() == 0) {
301 refreshTimer = scheduler.scheduleWithFixedDelay(() -> {
302 int remainingTime = nhcThermostat.getRemainingOverruletime();
303 updateState(CHANNEL_OVERRULETIME, new DecimalType(remainingTime));
304 if (remainingTime == 0) {
305 cancelRefreshTimer();
307 }, 1, 1, TimeUnit.MINUTES);
310 private void cancelRefreshTimer() {
311 ScheduledFuture<?> timer = refreshTimer;
319 public void thermostatInitialized() {
320 Bridge bridge = getBridge();
321 if ((bridge != null) && (bridge.getStatus() == ThingStatus.ONLINE)) {
322 updateStatus(ThingStatus.ONLINE);
327 public void thermostatRemoved() {
328 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
329 "@text/offline.configuration-error.thermostatRemoved");
332 private void restartCommunication(NikoHomeControlCommunication nhcComm) {
333 // We lost connection but the connection object is there, so was correctly started.
334 // Try to restart communication.
335 nhcComm.scheduleRestartCommunication();
336 // If still not active, take thing offline and return.
337 if (!nhcComm.communicationActive()) {
338 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
339 "@text/offline.communication-error");
342 // Also put the bridge back online
343 NikoHomeControlBridgeHandler nhcBridgeHandler = getBridgeHandler();
344 if (nhcBridgeHandler != null) {
345 nhcBridgeHandler.bridgeOnline();
347 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
348 "@text/offline.configuration-error.invalid-bridge-handler");
352 private @Nullable NikoHomeControlCommunication getCommunication(
353 @Nullable NikoHomeControlBridgeHandler nhcBridgeHandler) {
354 return nhcBridgeHandler != null ? nhcBridgeHandler.getCommunication() : null;
357 private @Nullable NikoHomeControlBridgeHandler getBridgeHandler() {
358 Bridge nhcBridge = getBridge();
359 return nhcBridge != null ? (NikoHomeControlBridgeHandler) nhcBridge.getHandler() : null;
363 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
364 ThingStatus bridgeStatus = bridgeStatusInfo.getStatus();
365 if (ThingStatus.ONLINE.equals(bridgeStatus)) {
367 scheduler.submit(this::startCommunication);
369 updateStatus(ThingStatus.ONLINE);
372 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);