]> git.basschouten.com Git - openhab-addons.git/blob
f73c043f0a4b8211059f5e2983d2fff5c5307c2b
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.luxtronikheatpump.internal;
14
15 import java.io.IOException;
16 import java.util.ArrayList;
17 import java.util.HashSet;
18 import java.util.List;
19 import java.util.Set;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22
23 import javax.measure.Unit;
24
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;
50
51 /**
52  * The {@link LuxtronikHeatpumpHandler} is responsible for handling commands, which are
53  * sent to one of the channels.
54  *
55  * @author Stefan Giehl - Initial contribution
56  */
57 @NonNullByDefault
58 public class LuxtronikHeatpumpHandler extends BaseThingHandler {
59
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;
66
67     public LuxtronikHeatpumpHandler(Thing thing, LuxtronikTranslationProvider translationProvider) {
68         super(thing);
69         this.translationProvider = translationProvider;
70         config = new LuxtronikHeatpumpConfiguration();
71     }
72
73     @Override
74     public void updateState(String channelID, State state) {
75         super.updateState(channelID, state);
76     }
77
78     @Override
79     public void updateProperty(String name, @Nullable String value) {
80         super.updateProperty(name, value);
81     }
82
83     public void setStatusConnectionError() {
84         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
85                 "couldn't establish network connection [host '" + config.ipAddress + "']");
86     }
87
88     public void setStatusOnline() {
89         updateStatus(ThingStatus.ONLINE);
90     }
91
92     @Override
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
98             return;
99         }
100
101         HeatpumpChannel channel;
102
103         try {
104             channel = HeatpumpChannel.fromString(channelId);
105         } catch (InvalidChannelException e) {
106             logger.debug("Channel '{}' could not be found for thing {}", channelId, thing.getUID());
107             return;
108         }
109
110         if (!channel.isWritable()) {
111             logger.debug("Channel {} is a read-only channel and cannot handle command '{}'", channelId, command);
112             return;
113         }
114
115         if (command instanceof QuantityType) {
116             QuantityType<?> value = (QuantityType<?>) command;
117
118             Unit<?> unit = channel.getUnit();
119             if (unit != null) {
120                 value = value.toUnit(unit);
121             }
122
123             command = new DecimalType(value.floatValue());
124         }
125
126         if (command instanceof OnOffType) {
127             command = ((OnOffType) command) == OnOffType.ON ? new DecimalType(1) : DecimalType.ZERO;
128         }
129
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());
133             return;
134         }
135
136         Integer param = channel.getChannelId();
137         Integer value = null;
138
139         switch (channel) {
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();
149                 break;
150             case CHANNEL_BA_HZ_AKT:
151             case CHANNEL_BA_BW_AKT:
152                 value = ((DecimalType) command).intValue();
153                 try {
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,
158                             e.getMessage());
159                     return;
160                 }
161                 break;
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);
170                 break;
171             case CHANNEL_EINST_BWSTYP_AKT:
172                 value = ((DecimalType) command).intValue();
173                 try {
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,
178                             e.getMessage());
179                     return;
180                 }
181                 break;
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);
186                 break;
187
188             default:
189                 logger.debug("Received unknown channel {}", channelId);
190                 break;
191         }
192
193         if (param != null && value != null) {
194             if (sendParamToHeatpump(param, value)) {
195                 logger.debug("Heat pump mode {} set to {}.", channel.getCommand(), value);
196             } else {
197                 logger.warn("Failed setting heat pump mode {} to {}", channel.getCommand(), value);
198             }
199         } else {
200             logger.warn("No valid value given for Heatpump operation {}", channel.getCommand());
201         }
202     }
203
204     @Override
205     public void initialize() {
206         config = getConfigAs(LuxtronikHeatpumpConfiguration.class);
207
208         if (!config.isValid()) {
209             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
210                     "At least one mandatory configuration field is empty");
211             return;
212         }
213
214         updateStatus(ThingStatus.UNKNOWN);
215
216         synchronized (scheduledFutures) {
217             ScheduledFuture<?> future = scheduler.scheduleWithFixedDelay(this::internalInitialize, 0,
218                     RETRY_INTERVAL_SEC, TimeUnit.SECONDS);
219             scheduledFutures.add(future);
220         }
221     }
222
223     private void internalInitialize() {
224         // connect to heatpump and check if values can be fetched
225         HeatpumpConnector connector = new HeatpumpConnector(config.ipAddress, config.port);
226
227         try {
228             connector.read();
229         } catch (IOException e) {
230             setStatusConnectionError();
231             return;
232         }
233
234         // stop trying to establish a connection for initializing the thing once it was established
235         stopJobs();
236
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);
240         }
241
242         setStatusOnline();
243         restartJobs();
244     }
245
246     @Override
247     protected void updateConfiguration(Configuration configuration) {
248         tiggerChannelUpdate = true;
249         super.updateConfiguration(configuration);
250     }
251
252     @Override
253     public void dispose() {
254         stopJobs();
255     }
256
257     private void updateChannels(HeatpumpConnector connector) {
258         Integer[] visibilityValues = connector.getVisibilities();
259         Integer[] heatpumpValues = connector.getValues();
260         Integer[] heatpumpParams = connector.getParams();
261
262         logger.debug("Updating available channels for thing {}", thing.getUID());
263
264         final ThingHandlerCallback callback = getCallback();
265         if (callback == null) {
266             logger.debug("ThingHandlerCallback is null. Skipping migration of last_update channel.");
267             return;
268         }
269
270         ThingBuilder thingBuilder = editThing();
271         List<Channel> channelList = new ArrayList<>();
272
273         // clear channel list
274         thingBuilder.withoutChannels(thing.getChannels());
275
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");
284             } else {
285                 channelTypeUID = new ChannelTypeUID(LuxtronikHeatpumpBindingConstants.BINDING_ID, channel.getCommand());
286             }
287             if ((channelId != null && length <= channelId)
288                     || (config.showAllChannels == Boolean.FALSE && !channel.isVisible(visibilityValues))) {
289                 logger.debug("Hiding channel {}", channel.getCommand());
290             } else {
291                 channelList.add(callback.createChannelBuilder(channelUID, channelTypeUID).build());
292             }
293         }
294
295         thingBuilder.withChannels(channelList);
296
297         updateThing(thingBuilder.build());
298     }
299
300     private void restartJobs() {
301         stopJobs();
302
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,
308                         TimeUnit.SECONDS);
309                 scheduledFutures.add(future);
310             }
311         }
312     }
313
314     private void stopJobs() {
315         synchronized (scheduledFutures) {
316             for (ScheduledFuture<?> future : scheduledFutures) {
317                 if (!future.isDone()) {
318                     future.cancel(true);
319                 }
320             }
321             scheduledFutures.clear();
322         }
323     }
324
325     /**
326      * Set a parameter on the Luxtronik heatpump.
327      *
328      * @param param
329      * @param value
330      */
331     private boolean sendParamToHeatpump(int param, int value) {
332         HeatpumpConnector connector = new HeatpumpConnector(config.ipAddress, config.port);
333
334         try {
335             return connector.setParam(param, value);
336         } catch (IOException e) {
337             setStatusConnectionError();
338         }
339
340         return false;
341     }
342 }