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.luxtronikheatpump.internal;
15 import java.io.IOException;
16 import java.util.ArrayList;
17 import java.util.HashSet;
18 import java.util.List;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
23 import javax.measure.Unit;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.luxtronikheatpump.internal.enums.HeatpumpChannel;
28 import org.openhab.binding.luxtronikheatpump.internal.enums.HeatpumpCoolingOperationMode;
29 import org.openhab.binding.luxtronikheatpump.internal.enums.HeatpumpOperationMode;
30 import org.openhab.binding.luxtronikheatpump.internal.exceptions.InvalidChannelException;
31 import org.openhab.binding.luxtronikheatpump.internal.exceptions.InvalidOperationModeException;
32 import org.openhab.core.config.core.Configuration;
33 import org.openhab.core.library.types.DecimalType;
34 import org.openhab.core.library.types.OnOffType;
35 import org.openhab.core.library.types.QuantityType;
36 import org.openhab.core.thing.Channel;
37 import org.openhab.core.thing.ChannelUID;
38 import org.openhab.core.thing.Thing;
39 import org.openhab.core.thing.ThingStatus;
40 import org.openhab.core.thing.ThingStatusDetail;
41 import org.openhab.core.thing.binding.BaseThingHandler;
42 import org.openhab.core.thing.binding.ThingHandlerCallback;
43 import org.openhab.core.thing.binding.builder.ThingBuilder;
44 import org.openhab.core.thing.type.ChannelTypeUID;
45 import org.openhab.core.types.Command;
46 import org.openhab.core.types.RefreshType;
47 import org.openhab.core.types.State;
48 import org.slf4j.Logger;
49 import org.slf4j.LoggerFactory;
52 * The {@link LuxtronikHeatpumpHandler} is responsible for handling commands, which are
53 * sent to one of the channels.
55 * @author Stefan Giehl - Initial contribution
58 public class LuxtronikHeatpumpHandler extends BaseThingHandler {
60 private final Logger logger = LoggerFactory.getLogger(LuxtronikHeatpumpHandler.class);
61 private final Set<ScheduledFuture<?>> scheduledFutures = new HashSet<>();
62 private static final int RETRY_INTERVAL_SEC = 60;
63 private boolean tiggerChannelUpdate = false;
64 private final LuxtronikTranslationProvider translationProvider;
65 private LuxtronikHeatpumpConfiguration config;
67 public LuxtronikHeatpumpHandler(Thing thing, LuxtronikTranslationProvider translationProvider) {
69 this.translationProvider = translationProvider;
70 config = new LuxtronikHeatpumpConfiguration();
74 public void updateState(String channelID, State state) {
75 super.updateState(channelID, state);
79 public void updateProperty(String name, @Nullable String value) {
80 super.updateProperty(name, value);
83 public void setStatusConnectionError() {
84 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
85 "couldn't establish network connection [host '" + config.ipAddress + "']");
88 public void setStatusOnline() {
89 updateStatus(ThingStatus.ONLINE);
93 public void handleCommand(ChannelUID channelUID, Command command) {
94 String channelId = channelUID.getIdWithoutGroup();
95 logger.debug("Handle command '{}' for channel {}", command, channelId);
96 if (command == RefreshType.REFRESH) {
97 // ignore resresh command as channels will be updated automatically
101 HeatpumpChannel channel;
104 channel = HeatpumpChannel.fromString(channelId);
105 } catch (InvalidChannelException e) {
106 logger.debug("Channel '{}' could not be found for thing {}", channelId, thing.getUID());
110 if (!channel.isWritable()) {
111 logger.debug("Channel {} is a read-only channel and cannot handle command '{}'", channelId, command);
115 if (command instanceof QuantityType) {
116 QuantityType<?> value = (QuantityType<?>) command;
118 Unit<?> unit = channel.getUnit();
120 value = value.toUnit(unit);
123 command = new DecimalType(value.floatValue());
126 if (command instanceof OnOffType) {
127 command = ((OnOffType) command) == OnOffType.ON ? new DecimalType(1) : DecimalType.ZERO;
130 if (!(command instanceof DecimalType)) {
131 logger.warn("Heatpump operation for item {} must be from type: {}. Received {}", channel.getCommand(),
132 DecimalType.class.getSimpleName(), command.getClass());
136 Integer param = channel.getChannelId();
137 Integer value = null;
140 case CHANNEL_EINST_BWTDI_AKT_MO:
141 case CHANNEL_EINST_BWTDI_AKT_DI:
142 case CHANNEL_EINST_BWTDI_AKT_MI:
143 case CHANNEL_EINST_BWTDI_AKT_DO:
144 case CHANNEL_EINST_BWTDI_AKT_FR:
145 case CHANNEL_EINST_BWTDI_AKT_SA:
146 case CHANNEL_EINST_BWTDI_AKT_SO:
147 case CHANNEL_EINST_BWTDI_AKT_AL:
148 value = ((DecimalType) command).intValue();
150 case CHANNEL_BA_HZ_AKT:
151 case CHANNEL_BA_BW_AKT:
152 value = ((DecimalType) command).intValue();
154 // validate the value is valid
155 HeatpumpOperationMode.fromValue(value);
156 } catch (InvalidOperationModeException e) {
157 logger.warn("Heatpump {} mode recevieved invalid value {}: {}", channel.getCommand(), value,
162 case CHANNEL_EINST_WK_AKT:
163 case CHANNEL_EINST_BWS_AKT:
164 case CHANNEL_EINST_KUCFTL_AKT:
165 case CHANNEL_SOLLWERT_KUCFTL_AKT:
166 case CHANNEL_SOLL_BWS_AKT:
167 case CHANNEL_EINST_HEIZGRENZE_TEMP:
168 float temperature = ((DecimalType) command).floatValue();
169 value = (int) (temperature * 10);
171 case CHANNEL_EINST_BWSTYP_AKT:
172 value = ((DecimalType) command).intValue();
174 // validate the value is valid
175 HeatpumpCoolingOperationMode.fromValue(value);
176 } catch (InvalidOperationModeException e) {
177 logger.warn("Heatpump {} mode recevieved invalid value {}: {}", channel.getCommand(), value,
182 case CHANNEL_EINST_KUHL_ZEIT_EIN_AKT:
183 case CHANNEL_EINST_KUHL_ZEIT_AUS_AKT:
184 float hours = ((DecimalType) command).floatValue();
185 value = (int) (hours * 10);
189 logger.debug("Received unknown channel {}", channelId);
193 if (param != null && value != null) {
194 if (sendParamToHeatpump(param, value)) {
195 logger.debug("Heat pump mode {} set to {}.", channel.getCommand(), value);
197 logger.warn("Failed setting heat pump mode {} to {}", channel.getCommand(), value);
200 logger.warn("No valid value given for Heatpump operation {}", channel.getCommand());
205 public void initialize() {
206 config = getConfigAs(LuxtronikHeatpumpConfiguration.class);
208 if (!config.isValid()) {
209 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
210 "At least one mandatory configuration field is empty");
214 updateStatus(ThingStatus.UNKNOWN);
216 synchronized (scheduledFutures) {
217 ScheduledFuture<?> future = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0,
218 RETRY_INTERVAL_SEC, TimeUnit.SECONDS);
219 scheduledFutures.add(future);
223 private void internalInitialize() {
224 // connect to heatpump and check if values can be fetched
225 HeatpumpConnector connector = new HeatpumpConnector(config.ipAddress, config.port);
229 } catch (IOException e) {
230 setStatusConnectionError();
234 // stop trying to establish a connection for initializing the thing once it was established
237 // When thing is initialized the first time or and update was triggered, set the available channels
238 if (thing.getProperties().isEmpty() || tiggerChannelUpdate) {
239 updateChannels(connector);
247 protected void updateConfiguration(Configuration configuration) {
248 tiggerChannelUpdate = true;
249 super.updateConfiguration(configuration);
253 public void dispose() {
257 private void updateChannels(HeatpumpConnector connector) {
258 Integer[] visibilityValues = connector.getVisibilities();
259 Integer[] heatpumpValues = connector.getValues();
260 Integer[] heatpumpParams = connector.getParams();
262 logger.debug("Updating available channels for thing {}", thing.getUID());
264 final ThingHandlerCallback callback = getCallback();
265 if (callback == null) {
266 logger.debug("ThingHandlerCallback is null. Skipping migration of last_update channel.");
270 ThingBuilder thingBuilder = editThing();
271 List<Channel> channelList = new ArrayList<>();
273 // clear channel list
274 thingBuilder.withoutChannels(thing.getChannels());
276 // create list with available channels
277 for (HeatpumpChannel channel : HeatpumpChannel.values()) {
278 Integer channelId = channel.getChannelId();
279 int length = channel.isWritable() ? heatpumpParams.length : heatpumpValues.length;
280 ChannelUID channelUID = new ChannelUID(thing.getUID(), channel.getCommand());
281 ChannelTypeUID channelTypeUID;
282 if (channel.getCommand().matches("^channel[0-9]+$")) {
283 channelTypeUID = new ChannelTypeUID(LuxtronikHeatpumpBindingConstants.BINDING_ID, "unknown");
285 channelTypeUID = new ChannelTypeUID(LuxtronikHeatpumpBindingConstants.BINDING_ID, channel.getCommand());
287 if ((channelId != null && length <= channelId)
288 || (config.showAllChannels == Boolean.FALSE && !channel.isVisible(visibilityValues))) {
289 logger.debug("Hiding channel {}", channel.getCommand());
291 channelList.add(callback.createChannelBuilder(channelUID, channelTypeUID).build());
295 thingBuilder.withChannels(channelList);
297 updateThing(thingBuilder.build());
300 private void restartJobs() {
303 synchronized (scheduledFutures) {
304 if (getThing().getStatus() == ThingStatus.ONLINE) {
305 // Repeat channel update job every configured seconds
306 Runnable channelUpdaterJob = new ChannelUpdaterJob(this, translationProvider);
307 ScheduledFuture<?> future = scheduler.scheduleWithFixedDelay(channelUpdaterJob, 0, config.refresh,
309 scheduledFutures.add(future);
314 private void stopJobs() {
315 synchronized (scheduledFutures) {
316 for (ScheduledFuture<?> future : scheduledFutures) {
317 if (!future.isDone()) {
321 scheduledFutures.clear();
326 * Set a parameter on the Luxtronik heatpump.
331 private boolean sendParamToHeatpump(int param, int value) {
332 HeatpumpConnector connector = new HeatpumpConnector(config.ipAddress, config.port);
335 return connector.setParam(param, value);
336 } catch (IOException e) {
337 setStatusConnectionError();